四个进阶排序,入门看这个就够了

346 阅读6分钟

相比于冒泡排序、选择排序、插入排序这个三比较简单的排序来说,还有这个四个更强的进阶排序:希尔排序、堆排序、快速排序、归并排序。本篇文章会详细讲解这四个排序的思路与实现~ 初学者可以看看下面这篇文章~
三个简单排序

希尔排序

​ 希尔排序又称缩小增量排序, 希尔排序也是对直接插入排序的一种优化~ 希尔排序的基本思想是:先选定一个数,把待排序数组中所有元素分成若干个组,所有距离 gap 相同的数分在同一组内,并对每一组内的数据进行直接插入排序。然后重复上述分组排序的工作, 当 gap = 1 时,对整体进行了一次插入排序

  • 当 gap = 1 时, 就是正常的插入排序
  • 当 gap != 时的排序都称作是预排序
  • 每次预排序 i 从 gap 开始
  • 希尔排序利用了, 直接插入排序的待排序数据越有序越快的这一特性

图解

第一次排序

第二次排序

第三次排序

代码

public void shellSort(int[] arr) {
    if (arr == null || arr.length < 2) {
        return;
    }
    int gap = arr.length / 2;
    while (gap > 1) {
        // 这里的排序都是预排序
        shell(arr, gap);
        gap /= 2;
    }
    // 最后 gap = 1 时对整体再进行一次排序
    shell(arr, 1);
}

private void shell(int[] arr, int gap) {
    for (int i = gap; i < arr.length; i++) {
        int temp = arr[i];
        int j = i - gap;
        for (; j >= 0; j -= gap) {
            if (arr[j] > temp) {
                arr[j + gap] = arr[j];
            } else {
                break;
            }
        }
        arr[j + gap] = temp;
    }
}

分析

空间复杂度: O(1)

时间复杂度: O(n ^ 1.3 ~ n ^ 1.5)

  • 希尔排序的时间复杂度和你所设置的增量(即 gap )密切相关的

稳定性: 不稳定

  • 看在比较的过程当中 是否发生了跳跃式的交换 如果发生了跳跃式的交换 那么就是不稳定的排序

希尔排序的特性总结:

  1. 希尔排序是对直接插入排序的优化。
  2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果
  3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定

堆排序

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

基本思路:

  1. 建堆
  2. 交换首元素和尾元素, 然后对堆进行向下调整

代码

public void heapSort(int[] arr) {
    if (arr == null || arr.length < 2) {
        return;
    }
    // 1.建堆
    createHeap(arr);
    // 2.交换首元素和尾元素, 然后对堆进行向下调整
    // 向下调整的空间复杂度为 O(N * logN)
    int end = arr.length - 1;
    while (end >= 0) {
        // 交换
        swap(arr, 0, end);
        // 调整
        shiftDown(arr, 0, end);
        end--;
    }
}

private void swap(int[] arr, int x ,int y) {
    int temp = arr[x];
    arr[x] = arr[y];
    arr[y] = temp;
}

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

private void shiftDown(int[] arr, int parent, int len) {
    int child = parent * 2 + 1;
    while (child > len) {
        if (child + 1 < len && arr[child] < arr[child + 1]) {
            // 保证 child 下标的值是左右孩子的最大值
            child++;
        }
        if (arr[child] > arr[parent]) {
            swap(arr, parent, child);
            parent = child;
            child = parent * 2 + 1;
        } else {
            break;
        }
    }
}

分析

空间复杂度: O(1)

时间复杂度: O(N*logN)

稳定性: 不稳定

快速排序

快排的关键点就在于如何找基准, 这里有三种找基准的方法

  • Hoare 法
  • 挖坑法
  • 前后指针法

快排这里需注意, 如果你给的数据量太大了, 它可能会抛出栈溢出异常, 因为你递归的深度太深了, 把栈挤爆了

Hoare 法

基本思路:

  1. 取待排序元素序列中的最左边(或最右边)元素作为基准值
    • 如果取最左边的数作为基准值就得从右边开始走
    • 如果取最右边的数作为基准值就得从左边开始走
  2. 按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值
  3. 然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止

图解

代码

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

private void qSort(int[] arr, int left, int right) {
    if (left >= right) {
        return;
    }
    // 找基准
    int pivot = partition(arr, left, right);
    // 先递归左后递归右
    qSort(arr, 0, pivot - 1);
    qSort(arr, pivot, arr.length - 1);
}

private int partition(int[] arr, int left, int right) {
    int n = left;
    int temp = arr[left];
    while (left < right) {
        while (left < right && arr[right] >= temp) {
            right--;
        }
        // 此时 right 下标就是要交换的数字
        while (left < right && arr[left] <= temp) {
            left++;
        }
        // 此时 left 下标就是要交换的数字
        swap(arr, right, left);
    }
    // 此时的 left 和 right 相遇, 就和 最左边的基准数交换
    // 交换完成后并返回该基准数的下标
    swap(arr, n, left);
    return left;
}

private void swap(int[] arr, int x ,int y) {
    int temp = arr[x];
    arr[x] = arr[y];
    arr[y] = temp;
}

挖坑法

基本思路:

  1. 从最左或最右边 "挖个坑" 作为第一次的基准值, 将数值放入 temp 中
  2. 从右或左开始走, 右边遇到比 temp 中的值大的就往左走, 遇到比 temp 中的值小就放入前面第一次挖的坑中
  3. 同理, 左边遇到比 temp 中的值小的就往右走, 遇到比 temp 中的值大就放入前面挖的坑中, 直到 left 和 right 相遇

图解

代码

挖坑法的找基准代码如下:

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

前后指针法

图解

代码

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

分析

对于以上的这三个代码来说, 它们的空间复杂度时间、复杂度和稳定性都是一样的

空间复杂度: O(logN) ~ O(N)

  • 最坏情况: O(N) 即此时是一颗单分支二叉树
  • 最好情况: O(logN) 每次可以均匀的分割待排序序列, 对半分割~

时间复杂度:

  • 最坏情况下: O(N ^ 2) 因为此时的待排序序列(即序列有序的情况下)就为一颗单分支的二叉树, 递归的深度就是元素个数 N
  • 最好情况下: O(N * logN) 此时的待排序序列是一个完全二叉树(即每次可以均匀的分割待排序序列), 递归的深度就是树的高度 logN

稳定性: 不稳定

归并排序

​ 归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并

图解

分解路线

归并路线

代码

public void mergeSort(int[] arr) {
    if (arr == null || arr.length > 2) {
        return;
    }
    mergeSortInternal(arr, 0, arr.length);
}

private void mergeSortInternal(int[] arr, int left, int right) {
    if (left < right) {
        return;
    }
    int mid = left + right / 2;
    mergeSortInternal(arr, left, mid);
    mergeSortInternal(arr, mid + 1, right);
    merge(arr, left, mid, right);
}

private void merge(int[] arr, int left, int mid, int right) {
    int s1 = left;
    int e1 = mid;
    int s2 = mid + 1;
    int e2 = right;

    int[] newArray = new int[right - left + 1];
    int k = 0;// newArray 的下标
    while (s1 <= e1 && s2 <= e2) {
        if (arr[s1] <= arr[s2]){
            newArray[k++] = arr[s1++];
        } else {
            newArray[k++] = arr[s2++];
        }
    }
    while (s1 <= e1) {
        newArray[k++] = arr[s1++];
    }
    while (s2 <= e2) {
        newArray[k++] = arr[s2++];
    }
    for (int i = 0; i < newArray.length; i++) {
        arr[i + left] = newArray[i];
    }
}

分析

空间复杂度: O(N)

时间复杂度: O(N ^ logN)

  • 归并排序的时间复杂度无论是在最好情况下还是在最坏情况下, 都是 O(N ^ logN)
  • 因为每次的分割都是均匀的从中间位置分割的~

稳定性: 稳定

  • 稳定的排序有: 插入、冒泡、归并

总结

排序方法最好平均最坏空间复杂度稳定性
冒泡排序O(n)O(n^2)O(n^2)O(1)稳定
插入排序O(n)O(n^2)O(n^2)O(1)稳定
选择排序O(n^2)O(n^2)O(n^2)O(1)不稳定
希尔排序O(n)O(n^1.3)O(n^2)O(1)不稳定
堆排序O(n * log(n))O(n * log(n))O(n * log(n))O(1)不稳定
快速排序O(n * log(n))O(n * log(n))O(n^2)O(log(n)) ~ O(n)不稳定
归并排序O(n * log(n))O(n * log(n))O(n * log(n))O(n)稳定