七大比较排序算法

1. 插入排序

1.1 直接插入排序

直接插入排序的基本思想:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列

举个例子,当我们玩扑克牌时就是使用直接插入排序的,假如我们现在有一大堆牌,我们想去整理时,使用的就是插入排序

举个例子:
将8 5 3 5按照从小到大的顺序排序

  1. 我们可以将这四个数看作一张张扑克牌
  2. 我们依次抓取一张扑克牌
  3. 在抓取第一张牌8时,本身就是有序的,因此不需要排序
  4. 在抓取第二张牌5时,将其进行插入到合适的位置
  5. 在抓取第三张牌3时,前面两张牌都是有序的,将第三张牌插入到合适的位置
  6. 在抓取第四张牌5时,前面三张牌都是有序的,将第四张牌插入到合适的位置

通过这个示例,我们就可以知道直接插入排序的本质:
当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移

代码实现:

	public void insertSort(int[] arr) {
        for(int i = 1; i < arr.length; i++) {
            int j = i - 1;
            int tmp = arr[i];
            while(j >= 0) {
                if(arr[j] > tmp) {
                    arr[j+1] = arr[j];
                }else {
                    break;
                }
                j--;
            }
            arr[j+1] = tmp;
        }
    }

直接插入排序的特性:

  1. 元素集合越接近有序,直接插入排序的速度就越快
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:稳定,直接插入排序是一种稳定的排序算法

注:
稳定性就是指,同等大小的元素,经排序后,是否还是原来的顺序。就比如上面的8 5 3 5,经直接插入排序后,结果为3 5 5 8,5还是在5前面,故直接插入排序是一种稳定的排序算法

那么如果我们将代码中arr[j] > tmp改为arr[j] >= tmp 这时,结果就是3 5 5 8.这时,可能就有人质疑直接插入排序的稳定性了。其实,如果一个排序本身就是一个稳定的排序,我们可以将其实现为不稳定的排序,而该排序本事是稳定的;如果一个排序本身就是不稳定,是无法将其实现为稳定的排序

1.2 希尔排序(缩小增量排序)

希尔排序法又称缩小增量法。希尔排序的基本思想是:先将整个待排记录序列分割成若干个子序列,将子序列分别进行直接插入排序。然后,再将该序列分割,只不过这次分割后得到的子序列个数要比上一次的子序列数少,再各个子序列分别进行直接插入排序。依次不断循环,直至最后子序列就是原序列,这时进行直接插入排序,得到排序好的结果

举个例子:

在这里插入图片描述
通过这个例子,我们可以知道,原序列的分组是跳跃式分组的,以gap为距离,第一次是以gap = 5为距离来分组的,子序列分别为:{9,4},{1,4},{2,8},{5,3},{7,5},将各个子序列依次进行直接插入排序,后续也是按照这个规律进行的,只不过分组的gap在缩小,直至gap缩小至1,这时对全体记录进行直接插入排序

这时,可能就有人会有疑问了,最后还是要全体记录进行直接插入排序,那前面的操作的意义何在?还不如直接对原序列进行直接插入排序呢
答: 仔细观察每次分组排序后的序列,不难发现,数较大的在往后靠,数小的在往前靠,序列逐渐趋于有序了,而直接插入排序的特性就是元素集合越有序,速度就越快。当最后对全体记录进行直接插入排序时,序列就已经趋于有序了,这时进行直接插入排序,就会很快

因此

  1. 希尔排序本质上,就是对直接插入排序的优化
  2. 当gap > 1时都是预排序,目的是让序列更趋于有序。当gap==1时,序列已经接近于有序了,这样就会很快。这样整体而言,可以达到优化的效果
  3. 希尔排序的时间复杂度并不固定,这是由于gap的取值方法有很多
  4. 稳定性:不稳定

代码实现

    public void shellSort(int[] arr) {
        int gap = arr.length;
        while(gap > 0) {
            gap /= 2;
            //每个序列交替进行排序
            for(int i = gap; i < arr.length;i++) {
                int tmp = arr[i];
                int j = i - gap;
                while(j >= 0) {
                    if(arr[j] > tmp) {
                        arr[j+gap] = arr[j];
                    }else {
                        break;
                    }
                    j -= gap;
                }
                arr[j+gap] = tmp;
            }
        }
    }

2. 选择排序

基本思想: 每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完

2.1 直接选择排序

  1. 在元素集合array[i]~array[n-1]中选择最小(最大)的数据元素
  2. 若它不是这组元素中的第一个(最后一个)元素,则将它与这组元素中的第一个(最后一个)元素交换
  3. 在剩余的array[i+1]~array[n-1](array[i]~array[n-2])集合中,重复上述步骤,直至集合剩余1个元素

本质上,就是每次从集合中挑选中最小(最大)的元素,接着从剩余元素所组成的集合中挑选出最小(最大)的元素,依次进行,直至剩余一个元素

代码实现:
实现方式一:

    public void selectSort(int[] arr) {
        for(int i = 0; i < arr.length; i++) {
            int minIndex = i;
            for(int j = i+1; j < arr.length; j++) {
                if(arr[minIndex] > arr[j]) {
                    minIndex = j;
                }
            }
            swap(arr,i,minIndex);
        }
    }

    private void swap(int[] arr, int i, int minIndex) {
        int tmp = arr[i];
        arr[i] = arr[minIndex];
        arr[minIndex] = tmp;
    }

实现方式二

	public void selectSort(int[] arr) {
        int left = 0;
        int right = arr.length-1;
        while(left < right) {
            int minIndex = left;
            int maxIndex = right;
            for(int i = left+1; i <= right; i++) {
                if(arr[i] < arr[minIndex]) {
                    minIndex = i;
                }
                if(arr[i] > arr[maxIndex]) {
                    maxIndex = i;
                }
            }
            swap(arr, minIndex, left);
            //最大值正好是  left下标  此时 把最大值换到了minIndex的位置了!!!
            if(maxIndex == left) {
                maxIndex = minIndex;
            }
            swap(arr, maxIndex, right);
            left++;
            right--;
        }
    }

直接选择排序的特性:

  1. 直接选择排序比较容易理解,但是时间效率不高,平常很少使用
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定

在直接选择排序的过程中,如果我们找到一个新的最小元素,通常会与当前已排序部分的最后一个元素交换位置。这种交换可能会导致相同值的元素相对顺序发生改变。因此,直接选择排序是一种不稳定的排序算法

2.2 堆排序

堆排序是指指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大根堆,拍降序建小根堆

详情见:堆排序和Top-K问题

代码如下

	private void swap(int[] arr, int i, int minIndex) {
        int tmp = arr[i];
        arr[i] = arr[minIndex];
        arr[minIndex] = tmp;
    }
    
    public void heapSort(int[] arr) {
        createHeap(arr);
        int end = arr.length-1;
        while(end > 0) {
            swap(arr, 0, end);
            siftDown(arr, 0, end);
            end--;
        }
    }

    private void createHeap(int[] arr) {
        for(int parent = (arr.length-1-1)/2; parent >= 0; parent--) {
            siftDown(arr, parent, arr.length);
        }
    }

    private void siftDown(int[] arr, int parent, int length) {
        int childMax = 2*parent+1;
        while(childMax < length) {
            if(childMax+1 < length && arr[childMax] < arr[childMax+1]) {
                childMax = childMax+1;
            }
            if(arr[parent] < arr[childMax]) {
                swap(arr, parent, childMax);
                parent = childMax;
                childMax = 2*parent+1;
            }else {
                break;
            }
        }
    }

堆排序特性:

  1. 堆排序使用堆来选数,效率比起直接选择排序就高了很多
  2. 时间复杂度:O(N*log2N)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定

3. 交换排序

基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来交换这两个记录在序列中的位置,交换排序的特点:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动

3.1 冒泡排序

    public void bubbleSort(int[] arr) {
    	//趟数
        for(int i = 0; i < arr.length; i++) {
        	//比较
            for(int j = 0; j < arr.length-1; j++) {
                if(arr[j] > arr[j+1]) {
                    swap(arr, j, j+1);
                }
            }
        }
    }

外循环是循环的趟数,内循环是依次比较相邻两个数,并将较大的数往后靠。因此,每次内循环结束,就将“最大值”放到了尾部

优化后的冒泡排序代码:

	public void bubbleSort(int[] arr) {
        for(int i = 0; i < arr.length; i++) {
            boolean flag = true;
            for(int j = 0; j < arr.length-1-i; j++) {
                if(arr[j] > arr[j+1]) {
                    swap(arr, j, j+1);
                    flag = false;
                }
            }
            if(flag) {
                break;
            }
        }
    }
  1. 定义了一个boolean类型的标记变量,因为有可能冒泡排序排到一半,数组就已经是有序的了
  2. 因为每个都会将“最大值”放到尾部,所以后续的内循环的比较可以不用比较后面已经排好序的“最大值”,所以可以将j<arr.length-1优化为j < arr.length-1-i

冒泡排序的特性

  1. 时间复杂度:O(N^2)
  2. 空间复杂度:O(1)
  3. 稳定性:稳定

3.2 快速排序

快速排序是一种基于二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两个子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后左右子序列重复该过程,知道所有元素都排列在相应位置上为止

下面以从小到大排序为例

先以Hoare法为例,H具体规则如下:

  1. 选择数组最左边的元素为基准值
  2. 从数组后面找到比基准值小的数据arr[right],停下来
  3. 从数组前面找到比基准值大的数据arr[left],停下来
  4. 交换数组中的这两个数据
  5. 重复2.3.4,当left和right相遇时,立刻停止
  6. 交换基准值和相遇点对应的数据值。这时可以发现,基准值的左边都比基准值小,基准值的右边都比基准值大
  7. 递归去快速排序当前基准值的前面子序列和后面子序列

举个例子:
在这里插入图片描述
实现代码如下:

	public void quickSort(int[] arr) {
        quick(arr, 0, arr.length-1);
    }

    private void quick(int[] arr, int start, int end) {
        if(start >= end) {
            return;
        }
        int pivot = partition(arr, start, end);//得到相遇与基准值交换后,基准值的位置
        quick(arr, start, pivot-1);//递归排序基准值左边的子序列
        quick(arr, pivot+1, end);//递归实现基准值右边的子序列
    }

将区间按照基准值划分为左右两半部分的常见方式有:

1. Hoare版

实现代码如下:

    private int partition1(int[] arr, int left, int right) {
        int tmp = arr[left];//记录当前的基准值
        int tmpLeft = left;//记录当前基准值的位置
        while(left < right) {
        	//从后开始找到比基准值小的值停下来
            while(left < right && tmp <= arr[right]) {//注意这里必须是<=,不能是<
                right--;
            }
            //从前开始找到比基准值大的值停下来
            while(left < right && tmp >= arr[left]) {//注意这里必须是>=,不能是>
                left++;
            }
            swap(arr, left, right);//交换这两个数据
        }
        swap(arr, tmpLeft, left);//交换基准值与相遇位置的值
        return left;//返回基准值的新位置
    }
    
    private void swap(int[] arr, int i, int minIndex) {
        int tmp = arr[i];
        arr[i] = arr[minIndex];
        arr[minIndex] = tmp;
    }

那么,为什么是先从后往前找到比基准值小的值停下来,再从前往后找到比基准值大的值停下来呢?能不能先从前往后,再从后往前呢

答: left和right相遇时的值,需要与基准值交换,而基准值都选择了序列最左边的数,为了保证交换后,基准值的左边都比它小,需要先从后边往前找到比基准值小的,这样,当left和right相遇时,对应的值必定比基准值小。如果先从前往后,再从后往前,当left和right相遇时,对应的值必定比基准值大,这样去交换,就不满足基准值的左边都比它小了

2. 挖坑法
将数组第一个元素放入一个临时变量tmp中,形成一个坑位

  1. 将数组中第一个元素,即arr[left]放入一个临时变量tmp中,并以此为基准值
  2. 从后找到比基准值小的基准值arr[right]时停下来,将其填入arr[left]中
  3. 从前找到比基准值大的基准值arr[left]时停下来,将其填入arr[right]
  4. 重复2,3,4直至left与right相遇
  5. 递归去快速排序当前基准值的前面子序列和后面子序列

实现代码:

	private int partition2(int[] arr, int left, int right) {
        int tmp = arr[left];
        while(left < right) {
            while(left < right && arr[right] >= tmp) {
                right--;
            }
            arr[left] = arr[right];
            while(left < right && arr[left] <= tmp) {
                left++;
            }
            arr[right] = arr[left];
        }
        arr[left] = tmp;
        return left;
    }

3. 前后指针法
实现代码:

private static int partition3(int[] array, int left, int right) {
    int prev = left ;
    int cur = left+1;
    while (cur <= right) {
        if(array[cur] < array[left] && array[++prev] != array[cur]) {
            swap(array,cur,prev);
        }
        cur++;
    }
    swap(array,prev,left);
    return prev;
 }

快速排序的优化

  1. “三数取中法”
    当需要排序的数组是一个有序的序列,那么利用快速排序,所生成的二叉树是一棵单分支的二叉树,这会导致递归的层次太深,从而导致栈溢出。那么怎么去优化这种情况呢?我们可以采用”三数取中法“来选取更合理的基准值

规则:定义一个中间位置下标min,mid =( left + right ) / 2,找到left,mid,right三个下标对应值的中间值(即第二大的值),将其与数组第一个元素交换位置,使它作为基准值
在这里插入图片描述
在这里插入图片描述
下面以在Hoare法的基础上利用”三数取中法“进行优化

	private int partition1(int[] arr, int left, int right) {
        int midIndex = getMiddleNumIndex(arr, left, right);//获取”中间值“的下标
        swap(arr, midIndex, left);//交换”中间值“与第一个位置的值
        int tmp = arr[left];
        int tmpLeft = left;
        while(left < right) {
            while(left < right && tmp <= arr[right]) {
                right--;
            }
            while(left < right && tmp >= arr[left]) {
                left++;
            }
            swap(arr, left, right);
        }
        swap(arr, tmpLeft, left);
        return left;
    }
    
	private int getMiddleNumIndex(int[] arr, int left, int right) {
        int mid = (left + right) / 2;
        if(arr[left] < arr[right]) {
            if(arr[mid] < arr[left]) {
                return left;
            }else if(arr[right] < arr[mid]) {
                return right;
            }else {
                return mid;
            }
        }else {
            if(arr[mid] < arr[right]) {
                return right;
            }else if(arr[left] < arr[mid]) {
                return left;
            }else {
                return mid;
            }
        }
    }
  1. 递归到小的子区间时,考虑使用插入排序
    由于快速排序是越排越有序的,因此在快速排序的后面几层,可采用插入排序,因为插入排序对于越有序的序列,排得越快
	private void quick(int[] arr, int start, int end) {
        if(start >= end) {
            return;
        }
        //  当子序列长度为7(可为其他值)时,使用插入排序对其进行排序
        if(end - start + 1 <= 7) {
            insertSortRange(arr, start, end);
            return;
        }
        int pivot = partition1(arr, start, end);
        quick(arr, start, pivot-1);
        quick(arr, pivot+1, end);
    }

	private void insertSortRange(int[] arr, int left, int right) {
        for(int i = left + 1; i <= right; i++) {
            int j = i - 1;
            int tmp = arr[i];
            while(j >= 0) {
                if(arr[j] > tmp) {
                    arr[j+1] = arr[j];
                }else {
                    break;
                }
                j--;
            }
            arr[j+1] = tmp;
        }
    }

快速排序的非递归实现
需要借助栈

	private void quickNor(int[] arr, int left, int right) {
        Deque<Integer> stack = new ArrayDeque<>();
        int pivot = partition1(arr, left, right);
        //分割后的子序列长度不为1,为1就没必要用partition了,因为left == right
        if(pivot - 1 > left) {
            stack.push(left);
            stack.push(pivot-1);
        }
        if(pivot + 1 < right) {
            stack.push(pivot+1);
            stack.push(right);
        }
        while(!stack.isEmpty()) {
            right = stack.pop();
            left = stack.pop();
            pivot = partition1(arr, left, right);
            if(pivot - 1 > left) {
                stack.push(left);
                stack.push(pivot-1);
            }
            if(pivot + 1 < right) {
                stack.push(pivot+1);
                stack.push(right);
            }
        }
    }

快速排序的特性:

  • 时间复杂度:O(N*log2N)
  • 空间复杂度:O(log2N)
  • 稳定性:不稳定

4. 归并排序

归并排序是先将数组不断向下均分成两个个子序列,直至两个子序列中都只有一个元素,此时这两个子序列都是有序的,再合并这两个有序序列,向上返回
在这里插入图片描述
实现代码:

	public void mergeSort(int[] arr) {
        mergeSortTmp(arr, 0, arr.length-1);
    }
    private void mergeSortTmp(int[] arr, int left, int right) {
        if(left >= right) return;
        //分解
        int mid = (left + right) / 2;
        mergeSortTmp(arr, left, mid);
        mergeSortTmp(arr, mid+1, right);

        //合并
        merge(arr, left, mid, right);
    }

    private void merge(int[] arr, int left, int mid, int right) {
        int[] tmp = new int[right-left+1];
        int k = 0;
        int s1 = left;
        int s2 = mid+1;
        while(s1 <= mid && s2 <= right) {
            if(arr[s1] <= arr[s2]) {
                tmp[k++] = arr[s1++];
            }else {
                tmp[k++] = arr[s2++];
            }
        }
        while(s1 <= mid) {
            tmp[k++] = arr[s1++];
        }
        while(s2 <= right) {
            tmp[k++] = arr[s2++];
        }

        for(int i = 0; i < k; i++) {
            arr[i+left] = tmp[i];//注意arr和tmp的下标映射关系,arr需要加一个left
        }
    }

归并排序的特性:

  1. 时间复杂度:递归的层数为log2N,每层合并的元素个数为N,因此时间复杂度为O(N * log2N)
  2. 空间复杂度:O(N)
  3. 稳定性:稳定

归并排序的非递归实现

	private void merge(int[] arr, int left, int mid, int right) {
        int[] tmp = new int[right-left+1];
        int k = 0;
        int s1 = left;
        int s2 = mid+1;
        while(s1 <= mid && s2 <= right) {
            if(arr[s1] <= arr[s2]) {
                tmp[k++] = arr[s1++];
            }else {
                tmp[k++] = arr[s2++];
            }
        }
        while(s1 <= mid) {
            tmp[k++] = arr[s1++];
        }
        while(s2 <= right) {
            tmp[k++] = arr[s2++];
        }

        for(int i = 0; i < k; i++) {
            arr[i+left] = tmp[i];//注意arr和tmp的下标映射关系,arr需要加一个left
        }
    }

    public void mergeSortNor(int[] arr) {
        int gap = 1;
        while(gap <= arr.length) {
            for(int i = 0; i < arr.length; i += 2 * gap) {
                int left = i;
                int mid = left + gap - 1;
                if(mid >= arr.length) {
                    mid = arr.length - 1;
                }
                int right = mid + gap;
                if(right >= arr.length - 1) {
                    right = arr.length - 1;
                }
                merge(arr, left, mid, right);
            }
            gap *= 2;
        }
    }

5. 总结

时间复杂度空间复杂度稳定性
插入排序O(N)O(N2)稳定
希尔排序O(N1.3)O(1)不稳定
选择排序O(N2)O(1)不稳定
堆排序O(N * log2N)O(1)不稳定
冒泡排序O(N2)O(1)稳定
快速排序O(N * log2N)O(log2N)不稳定
归并排序O(N * log2N)O(N)稳定
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值