常见排序方法详解(图示+方法)
一、插入排序
1.1基本思想
把待排序的记录
按其关键码值的大小逐个插入到一个已经排好序的有序序列中
,直到所有的记录插入完为止,得到
一个新的有序序列。
1.2直接插入排序
当插入第
i(i>=1)
个元素时,前面的
array[0],array[1],…,array[i-1]
已经排好序,此时用
array[i]
的排序码与
array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将
array[i]
插入,原来位置上的元素顺序后移。
示例代码如下:
class Sort{
public static void insertSort(int[] array){
//排序完成条件
for (int i = 1; i < array.length ; i++) {
//用于比较大小
int tmp = array[i];
//j的起始位置
int j =i-1;
//单次比较的结束条件
for(;j>=0;j--){
if(array[j]>tmp){
array[j+1]=array[j];
}else{
//由于前面的元素经排序后已经有序,故直接将tmp的值放入对应位置后,j无需在遍历前面的元素进行比较,
//直接跳出循环即可。
array[j+1]=tmp;
break;
}
}
array[j+1]=tmp;
}
}
}
直接插入排序的特性总结:
1.
元素集合
越接近有序
,直接插入排序算法的时间
效率越高
2.
时间复杂度:
O(N^2)
最坏情况:数据是逆序的;
最好情况:数据本身就是有序的;
结论:当一组数据
越接近于有序
,直接插入排序
越快
。
3.
空间复杂度:
O(1)
,它是一种
稳定
的排序算法
4.
稳定性:稳定
1.3希尔排序(缩小增量排序)
希尔排序法又称缩小增量法。希尔排序法的基本思想是:
先选定一个整数,把待排序文件中所有记录分成多个组,
所有距离为指定的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达
=1
时,所有记录在统一组内排好序
。
思路有了,代码如何实现?如何分组?
代码如下:
public static void shellSort(int[] array){
int gap=array.length;
while(gap>1){
gap/=2;
shell(array,gap);
}
}
private static void shell(int[] array, int gap) {
for (int i = gap; i <array.length ; i++) {
//增量为i++,使排序过程分组交替进行
int tmp = array[i];
int j = i-gap;
for (; j >=0 ; j-=gap) {
if(array[j]>tmp){
array[j+gap]=array[j];
}else{
array[j+gap]=tmp;
break;
}
}
array[j+gap]=tmp;
}
}
希尔排序的特性总结:
1.
希尔排序是
对直接插入排序的优化
。
2.
当
gap > 1时都是预排序
,目的是让数组更
接近于有序
。当
gap == 1
时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
3.
希尔排序的时间复杂度不好计算,因为
gap
的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定
4.
稳定性:
不稳定
二、选择排序
2.1直接选择排序
2.1.1基本思想
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元
素排完 。
2.2.2代码实现
图示:
代码演示:
public static void selectSort(int[] array){
for (int i = 0; i < array.length; i++) {
//每轮比较默认i下标的值为最小值
int minIndex=i;
for (int j =i+1; j <array.length ; j++) {
if(array[j]<array[minIndex]){
minIndex=j;
}
}
int tmp=array[i];
array[i]= array[minIndex];
array[minIndex]=tmp;
}
}
【
直接选择排序的特性总结
】
1.
直接选择排序思考非常好理解,但是
效率不是很好
。实际中
很少使用
2.
时间复杂度:
O(N^2)
3.
空间复杂度:
O(1)
4.
稳定性:
不稳定
2.2堆排序
堆排序思路在前面已经了解到,详情见文章中“堆的应用”部分。
https://blog.csdn.net/zhakakqns/article/details/141531282?spm=1001.2014.3001.5501https://blog.csdn.net/zhakakqns/article/details/141531282?spm=1001.2014.3001.5501
public static void heapSort(int[] array){
createHeap(array);
int end=array.length-1;
while(end>0) {
int tmp = array[0];
array[0] = array[end];
array[end] = tmp;
siftDown(array, 0, end);
end--;
}
}
private static void createHeap(int[] array){
//创建堆
for (int parent = (array.length-1-1)/2; parent >=0 ; parent--) {
siftDown(array,parent,array.length);
}
}
private static void siftDown(int[] array,int parent,int length){
//向下调整
int child = parent+1;
while(child<length){
if(child+1<length && array[child]<array[child+1]){
child++;
}
if(array[child]>array[parent]){
int tmp=array[parent];
array[parent]=array[child];
array[child]=tmp;
parent=child;
child=2*parent+1;
}else{
break;
}
}
}
【
直接选择排序的特性总结
】
1.
堆排序使用堆来选数,效率就高了很多。
2.
时间复杂度:
O(N*logN)
3.
空间复杂度:
O(1)
4.
稳定性:
不稳定
三、交换排序
3.1冒泡排序
3.1.1普通冒泡排序
public static void bubbleSort(int[] array){
for (int i = 0; i <array.length-1 ; i++) {
for (int j = 0; j < array.length-i-1; j++) {
if(array[j]>array[j+1]){
int tmp=array[j];
array[j]=array[j+1];
array[j+1]=tmp;
}
}
}
}
3.1.2冒泡排序优化
如果一组数据本身有序,或者在经过几趟冒泡排序后已经有序,就可以直接退出,不用继续执行代码。
public static void bubbleSort(int[] array){
for (int i = 0; i <array.length-1 ; i++) {
boolean flg=false;//用于判断是否发生交换,若没有交换,则已经有序直接退出
for (int j = 0; j < array.length-i-1; j++) {
if(array[j]>array[j+1]){
int tmp=array[j];
array[j]=array[j+1];
array[j+1]=tmp;
flg=true;//发生交换,置为true
}
}
if(!flg){
break;
}
}
}
!!!一般我们所说的冒泡排序的时间复杂度是指未优化过的冒泡排序。
【
冒泡排序的特性总结
】
1.
冒泡排序是一种非常容易理解的排序
2.
时间复杂度:
O(N^2)
3.
空间复杂度:
O(1)
4.
稳定性:
稳定
3.2快速排序
3.2.1快速排序
基本思想:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
3.2.1.1Hoare版
代码实现:
public static void quickSort(int[] array){
quick(array,0,array.length-1);
}
private static void quick(int[] array,int start,int end){
if(start>=end){
return;
}
//划分区域
int pivot = partition(array,start,end);
//递归左边
quick(array,start,pivot-1);
//递归右边
quick(array,pivot+1,end);
}
private static int partition(int[] array, int left, int right){
//以left下标的值作为基准
int key = left;
while(left<right) {
//如果数据为3,4,5,6那么right一直--也找不到比3大的数据
//,会导致越界,因此必须右left<right这个条件
while (left<right && array[right] >= array[key]) {
right--;
}
while (left<right && array[left] <= array[key]) {
left++;
}
int tmp = array[right];
array[right] = array[left];
array[left] = tmp;
}
int tmp = array[left];
array[left]=array[key];
array[key]=tmp;
return left;
}
【
特性总结】
1.
时间复杂度:
O(N^2)
当数据本身有序时,时间复杂度为n^2
最好情况
N*logN
2.
空间复杂度:
O(N)
3.
稳定性:
不
稳定
3.2.1.2挖坑法
递归的思路和Hoare法一样,也就是说只需要修改Hoare法的partition方法即可。
代码如下:
private static int partition(int[] array, int left, int right){
int tmp = array[left];
while(left<right) {
while (left<right && array[right] >= tmp) {
right--;
}
array[left] = array[right];
while (left<right && array[left] <= tmp) {
left++;
}
array[right] = array[left];
}
array[left] = tmp;
return left;
}
3.2.2快速排序优化
当一组数据有序时,快速排序的时间复杂度会达到O(N^2),并且当数据量过多时还会导致栈溢出,因此我们右以下几种优化方法:
(1)三数取中法选key
下面是寻找中间大小数据的方法:
private static int getMiddleNum(int[] array,int left,int right){
int mid = (left+right)/2;
if(array[left]<array[right]){
if(array[mid]<array[left]){
return left;
}else if(array[mid]>array[right]){
return right;
}else{
return mid;
}
}else{
if(array[mid]>array[left]){
return left;
}else if(array[mid]<array[right]){
return right;
}else{
return mid;
}
}
(2)递归到小的子区间时,可以考虑使用插入排序 ,但是在使用插入排序时,应注意排序的范围
i要从start+1开始,当j<start时结束该轮插入。
代码如下:
private static void quick(int[] array,int start,int end){
if(start>=end){
return;
}
//当数据小于10个时,使用直接插入排序
if(end-start+1<10){
insertSortRange(array,start,end);
return;
}
int pivot = partition(array,start,end);
quick(array,start,pivot-1);
quick(array,pivot+1,end);
}
private static void insertSortRange(int[] array, int start, int end) {
for (int i = start+1; i <=end ; i++) {
int tmp=array[i];
int j=i-1;
for(;j>=start;j--){
if(array[j]>tmp){
array[j+1]=array[j];
}else{
array[j+1]=tmp;
break;
}
}
array[j+1]=tmp;
}
}
3.2.3快速排序非递归
代码如下:
public static void quickNor(int[] array,int start,int end){
//首先先用pivot将两个子数组的start,end放入栈中
int pivot = partition(array,start,end);
Stack<Integer> stack = new Stack<>();
//如果pivot<=start+1,pivot>=end-1说明只剩下一个数据,则已经有序,不用再排序
if(pivot>start+1){
stack.push(start);
stack.push(pivot-1);
}
if(pivot<end-1){
stack.push(pivot+1);
stack.push(end);
}
while(!stack.isEmpty()){
end=stack.pop();
start=stack.pop();
pivot=partition(array,start,end);
if(pivot>start+1){
stack.push(start);
stack.push(pivot-1);
}
if(pivot<end-1){
stack.push(pivot+1);
stack.push(end);
}
}
}
3.2.4快速排序总结
1.
快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫
快速
排序
2.
时间复杂度:
O(N*logN)
(我们平时所说的快速排序的时间复杂度都是指优化过的)
3.
空间复杂度:
O(logN)
4.
稳定性:
不稳定
四、归并排序
归并排序(
MERGE-SORT
)是建立在归并操作上的一种有效的排序算法
,
该算法是采用
分治法
(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即
先使每个子序列有序,再使子序列段间有序
。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:
public static void mergeSort(int[] array){
mergeSortTmp(array,0,array.length-1);
}
private static void mergeSortTmp(int[] array, int left, int right) {
if (left >= right) {
return;
}
int mid = (left+right)/2;
mergeSortTmp(array,left,mid);
mergeSortTmp(array,mid+1,right);
//分解完毕,开始合并
merge(array,left,mid,right);
}
private static void merge(int[] array, int left, int mid, int right) {
//相当于合并连个有序数组
int[] tmp=new int[right-left+1];
int k=0;
int s1=left;
int e1=mid;
int s2=mid+1;
int e2=right;
while(s1<=e1 && s2<=e2){
if(array[s1]<=array[s2]){
tmp[k++]=array[s1++];
}else{
tmp[k++]=array[s2++];
}
}
while(s1<=e1){
tmp[k++]=array[s1++];
}
while(s2<=e2){
tmp[k++]=array[s2++];
}
for (int i = 0; i < k; i++) {
array[i+left]=tmp[i];
}
}
1.
归并的缺点在于需要
O(N)
的空间复杂度,归并排序的思考更多的是
解决在磁盘中的外排序
问题。
2.
时间复杂度:
O(N*logN)
3.
空间复杂度:
O(N)
4. 稳定性:稳定