8个排序算法

排序算法

排序算法(Sorting algorithms)是什么? Wikipedia 如是说:

In computer science, a sorting algorithm is an algorithm that puts elements of a list in a certain order.

也就是说,排序算法,就是某种算法,将列表中的元素按照某种规则排序。常见的如数字大小排序、字典序排序等。本系列例子约定为从小到大的数字排序,其他的类似,关键在于思路。

算法特性

1、内部排序和外部排序

按照数组规模的大小,排序可以分为内部排序和外部排序。

  • 内部排序(internal sorting): 全部数组都可放在内存中排序。
  • 外部排序(external sorting): 数组太大,不能全部放在内存中,部分数据在硬盘中。

本系列约定为内部排序,关于海量数据的排序,后续补充。

2、稳定性

排序法的稳定性(stability): 取决于值相等的两个元素,排序之后是否保持原来的顺序。

3、比较排序和非比较排序

比较排序(comparison sort):

比较排序中,每一步通过比较元素大小来决定元素的位置。其复杂度由比较次数和交换次数来决定。比较排序比较好实现,但是时间复杂度无法突破 O(nlogn)。证明过程,可以参考这篇文章

非比较排序(non-comparison sort):

非比较排序,如桶排序,不通过比较,后续将会讲解。这类算法可以突破 O(nlogn)。

排序算法有很多种,每一种都各自有自己的优点缺点和不同的应用场景,没有一种排序是绝对完美的。如何评价一个算法的优劣呢,我们通过算法复杂度来衡量。

常见排序算法总结

维基百科对排序算法的分类

本文并不会全部介绍,目前只介绍下表的8个,以后应该会继续增加(TODO)

排序算法

平均时间复杂度

最佳情况

最坏情况

空间复杂度

排序方式

稳定性

冒泡排序

O(n2)

O(n)

O(n2)

O(1)

In-place

稳定

选择排序

O(n2)

O(n2)

O(n2)

O(1)

In-place

不稳定

插入排序

O(n2)

O(n)

O(n2)

O(1)

In-place

稳定

希尔排序

O(nlogn)

O(nlog2n)

O(nlog2n)

O(1)

In-place

不稳定

归并排序

O(nlogn)

O(nlogn)

O(nlogn)

O(n)

Out-place

稳定

快速排序

O(nlogn)

O(nlogn)

O(n2)

O(logn)

In-place

不稳定

堆排序

O(nlogn)

O(nlogn)

O(nlogn)

O(1)

In-place

不稳定

基数排序

O(n*k)

O(n*k)

O(n*k)

O(n+k)

Out-place

稳定

 


到此,很多人会注意到基数排序的时间复杂度是最小的,那么为什么却没有快排、堆排序流行呢?我们看看下图算法导论的相关说明:

https://itimetraveler.github.io/gallery/sort-algorithms/radixsort-comparison.jpg

基数排序只适用于有基数的情况,而基于比较的排序适用范围就广得多。另一方面是内存上的考虑。作为一种通用的排序方法,最好不要带来意料之外的内存开销,所以各语言的默认实现都没有用基数排序,但是不能否认基数排序在各领域的应用。

常用的交换方法

冒泡排序,简单

概述

冒泡排序(Bubble Sort)是一种简单的排序算法。它重复访问要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。访问数列的工作是重复地进行直到没有再需要交换的数据,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端,像水中的气泡从水底浮到水面。

算法流程

冒泡排序算法的运作如下:

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

算法效率

排序算法

平均时间复杂度

最佳情况

最坏情况

空间复杂度

排序方式

稳定性

冒泡排序

O(n2)

O(n)

O(n2)

O(1)

In-place

稳定

代码实现

public static void bubbleSort(int[] array) {
    int length = array.length;
    for (int i = 0; i < length - 1; i++) {
        for (int j = 0; j < length - 1 - i; j++) {
            if (array[j] > array[j + 1]) {
                swap(array, j, j + 1);
            }
        }
    }
}

这个swap函数后面有可能会用到,就在这里写一次,下面的就不重复写了。

private static void swap(int[] array, int i, int j) {
    int temp = array[i];
    array[i] = array[j];
    array[j] = temp;
}

选择排序,简单

概述

选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对{\displaystyle n} 个元素的表进行排序总共进行至多{\displaystyle n-1}n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。

算法流程

  1. 从待排序序列中,找到关键字最小的元素;
  2. 如果最小元素不是待排序序列的第一个元素,将其和第一个元素互换;
  3. 从余下的 N – 1 个元素中,找出关键字最小的元素,重复①、②步,直到排序结束。

算法效率

排序算法

平均时间复杂度

最佳情况

最坏情况

空间复杂度

排序方式

稳定性

插入排序

O(n2)

O(n)

O(n2)

O(1)

In-place

稳定

代码实现

public static void selectSort(int[] array) {
    for (int i = 0; i < array.length; i++) {
        // 每一趟循环比较时,min用于存放较小元素的数组下标
        // 这样当前批次比较完毕最终存放的就是此趟内最小的元素的下标,避免每次遇到较小元素都要进行交换。
        int min = i;   
        // 找出最小值的下标,并复制给min
        for (int j = i + 1; j < array.length; j++) {
            if (array[j] < array[min]) {
                min = j;
            }
        }
        // 如果min发生变化,则进行交换
        if (min != i) {
            swap(array, min, i);
        }
    }
}

直接插入排序,简单

概述

插入排序(英语:Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到{\displaystyle O(1)}O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

 

算法流程

一般来说,插入排序都采用in-place在数组上实现。具体算法流程如下:

  1. 从第一个元素开始,该元素可以认为已经被排序
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
  5. 将新元素插入到该位置后
  6. 重复步骤2~5

如果比较操作的代价比交换操作大的话,可以采用二分查找法来减少比较操作的数目。该算法可以认为是插入排序的一个变种,称为二分查找插入排序。

算法效率

直接插入排序不是稳定的排序算法。

排序算法

平均时间复杂度

最佳情况

最坏情况

空间复杂度

排序方式

稳定性

插入排序

O(n2)

O(n)

O(n2)

O(1)

In-place

稳定

 

代码实现

提供两种写法,一种是移位法,一种是交换法。移位法是完全按照以上算法流程实,再插入过程中将有序序列中比待插入数字大的数据向后移动,由于移动时会覆盖待插入数据,所以需要额外的临时变量保存待插入数据,代码实现如下:

移位法

// 移位法
public static void insertionSort(int[] array) {
    for (int i = 0; i < array.length; i++) {
        int j = i - 1;
        int key = array[i];    // 先取出待插入数据保存,因为向后移位过程中会覆盖掉待插入数
        // 如果比待插入数据大,就后移
        while (j >= 0 && array[j] > key) {
            array[j + 1] = array[j];
            j--;
        }
        // 找到比待插入数据小的位置,将待插入数据插入
        array[j + 1] = key;
    }
}

交换法

交换法不 需求 额外的保存待插入数据,通过不停的向前交换带插入数据,类似冒泡法,直到找到比它小的值,也就是待插入数据找到了自己的位置。

// 交换法
public static void insertionSort2(int[] array) {
    for (int i = 1; i < array.length; i++) {
        int key = array[i];
        int j = i - 1;
        for (; j >= 0 && array[j] > key; j--) {
            array[j + 1] = array[j];
        }
        array[j + 1] = key;
    }
}

《编程珠玑》对插入排序的优化建议

https://blog.csdn.net/randyjiawenjie/article/details/6406556

希尔排序

概述

希尔排序(Shellsort,也称递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位

基本思想

  希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

  简单插入排序很循规蹈矩,不管数组分布是怎么样的,依然一步一步的对元素进行比较,移动,插入,比如[5,4,3,2,1,0]这种倒序序列,数组末端的0要回到首位置很是费劲,比较和移动元素均需n-1次。而希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序,小的基本在前,大的基本在后。然后缩小增量,到增量为1时,其实多数情况下只需微调即可,不会涉及过多的数据移动。

  我们来看下希尔排序的基本步骤,在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2...1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。

算法流程

  1. 选择一个增量序列t1,t2,…, tk ,其中ti>tj,tk=1;(一般初次取数组半长,之后每次再减半,直到增量为1)
  2. 按增量序列个数k,对序列进行k 趟排序;
  3. 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

 

在上面这幅图中: 初始时,有一个大小为 10 的无序序列。

在第一趟排序中,我们不妨设 gap1 = N / 2 = 5,即相隔距离为 5 的元素组成一组,可以分为 5 组。

接下来,按照直接插入排序的方法对每个组进行排序。

在第二趟排序中,我们把上次的 gap 缩小一半,即 gap2 = gap1 / 2 = 2 (取整数)。这样每相隔距离为 2 的元素组成一组,可以分为 2 组。

按照直接插入排序的方法对每个组进行排序。

在第三趟排序中,再次把 gap 缩小一半,即gap3 = gap2 / 2 = 1。 这样相隔距离为 1 的元素组成一组,即只有一组。 按照直接插入排序的方法对每个组进行排序。此时,排序已经结束。

需要注意一下的是,图中有两个相等数值的元素 5 和 5 。我们可以清楚的看到,在排序过程中,两个元素位置交换了。 所以,希尔排序是不稳定的算法。

算法效率

排序算法

平均时间复杂度

最佳情况

最坏情况

空间复杂度

排序方式

稳定性

希尔排序

O(nlogn)

O(nlog2n)

O(nlog2n)

O(1)

In-place

不稳定

代码实现

位移法

// 位移法
public static void shellSort(int[] array) {
    // 增量gap,并逐步缩小增量
    for (int gap = array.length / 2; gap > 0; gap /= 2) {
        // 从第gap个元素,逐个对其所在组进行直接插入排序操作
        for (int i = gap; i < array.length; i++) {
            int j = i;
            int temp = array[j];
            if (array[j] < array[j - gap]) {
                while (j - gap >= 0 && temp < array[j - gap]) {
                    // 移动法
                    array[j] = array[j - gap];
                    j -= gap;
                }
                array[j] = temp;
            }
        }
    }
}

交换法

// 交换法
public static void shellSort2(int[] array) {
    // 增量gap,并逐步缩小增量
    for (int gap = array.length / 2; gap > 0; gap /= 2) {
        // 从第gap个元素,逐个对其所在组进行直接插入排序操作
        for (int i = gap; i < array.length; i++) {
            int j = i;
            while (j - gap >= 0 && array[j] < array[j - gap]) {
                // 插入排序采用交换法
                swap(array, j, j - gap);
                j -= gap;
            }
        }
    }
}

归并排序

概述

归并排序(Merge-sort)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题(divide)成一些小的问题然后递归求解,而(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。

分而治之

归并排序是建立在归并操作上的一种有效的排序算法,1945年由约翰·冯·诺伊曼首次提出。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。

算法流程

  1. 将序列每相邻两个数字进行归并操作,形成 (n/2)个序列,排序后每个序列包含两个元素;
  2. 将上述序列再次归并,形成 (n/4)个序列,每个序列包含四个元素;
  3. 重复步骤②,直到所有元素排序完毕

算法效率

排序算法

平均时间复杂度

最佳情况

最坏情况

空间复杂度

排序方式

稳定性

归并排序

O(nlogn)

O(nlogn)

O(nlogn)

O(n)

Out-place

稳定

代码实现

递归法

public static void mergeSort(int[] a) {
    mergeSort(a, 0, a.length - 1);
}

private static void mergeSort(int[] a, int low, int high) {
    int mid = (low + high) / 2;
    if (low < high) {
        // 左边
        mergeSort(a, low, mid);
        // 右边
        mergeSort(a, mid + 1, high);
        // 左右归并
        merge(a, low, mid, high);
        System.out.println(Arrays.toString(a));
    }
}

/**
 * 将两个数组进行归并,归并前面2个数组已有序,归并后依然有序
 */
private static void merge(int[] a, int low, int mid, int high) {
    int[] temp = new int[high - low + 1];
    int i = low;// 左指针
    int j = mid + 1;// 右指针
    int k = 0;
    // 把较小的数先移到新数组中
    while (i <= mid && j <= high) {
        if (a[i] < a[j]) {
            temp[k++] = a[i++];
        } else {
            temp[k++] = a[j++];
        }
    }
    // 把左边剩余的数移入数组
    while (i <= mid) {
        temp[k++] = a[i++];
    }
    // 把右边边剩余的数移入数组
    while (j <= high) {
        temp[k++] = a[j++];
    }
    // 把新数组中的数覆盖nums数组
    for (int k2 = 0; k2 < temp.length; k2++) {
        a[k2 + low] = temp[k2];
    }
}

迭代法

// 非递归
public static void mergeSort2(int[] arr) {
    int len = 1;    // 初始排序数组的长度
    while (len < arr.length) {
        for (int i = 0;i < arr.length; i += len*2) {
            mergeSortHelper(arr, i, len);
        }
        len *= 2;   // 每次将排序数组的长度*2
    }
}
/**
  * 辅助函数
  * @param nums  原数组
  * @param start 从start位置开始
  * @param len  本次合并的数组长度
  */
private static void mergeSortHelper(int[] arr, int start, int len) {
    int[] temp = new int[len * 2];
    int i = start;
    int j = start + len;
    int k = 0;
    while(i < start + len && (j < start + len + len && j < arr.length)) {
        temp[k++] = arr[i] < arr[j]? arr[i++] : arr[j++];
    }
    while(i < start + len && i < arr.length) {  // 注意:这里i也可能超出长度
        temp[k++] = arr[i++];
    }
    while(j < start + len + len && j < arr.length) {
        temp[k++] = arr[j++];
    }
    int right = start + len + len;
    int index = 0;
    while(start < arr.length && start < right) {
        arr[start++] = temp[index++];
    }
}

快速排序

概述

快速排序(Quicksort)是对冒泡排序的一种改进,借用了分治的思想,由C. A. R. Hoare在1962年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以 递归 进行,以此达到整个数据变成有序序列。

算法流程

快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。

步骤为:

  1. 从数列中挑出一个元素,称为“基准”(pivot),
  2. 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任何一边)。在这个分割结束之后,该基准就处于数列的中间位置。这个称为分割(partition操作。
  3. 递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。

递归到最底部时,数列的大小是零或一,也就是已经排序好了。这个算法一定会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

算法效率

快速排序并不稳定,快速排序每次交换的元素都有可能不是相邻的, 因此它有可能打破原来值为相同的元素之间的顺序。

排序算法

平均时间复杂度

最佳情况

最坏情况

空间复杂度

排序方式

稳定性

快速排序

O(nlogn)

O(nlogn)

O(n2)

O(logn)

In-place

不稳定

代码实现

快速排序的几种实现方法有很多,我这里值只列举部分,详细请看下面

https://www.kancloud.cn/digest/pieces-algorithm/163612

https://blog.csdn.net/qq_29503203/article/details/53355185

https://segmentfault.com/a/1190000004410119

https://blog.csdn.net/MoreWindows/article/details/6684558

递归,挖坑法 用伪代码描述如下:

  1. low = L; high = R; 将基准数挖出形成第一个坑a[low]。
  2. high–,由后向前找比它小的数,找到后挖出此数填前一个坑a[low]中。
  3. low++,由前向后找比它大的数,找到后也挖出此数填到前一个坑a[high]中。
  4. 再重复执行②,③二步,直到low==high,将基准数填入a[low]中。
/**
 * 挖坑法
 */
public static void quickSort(int[] array) {
    quickSort(array, 0, array.length - 1);
}

private static void quickSort(int[] array, int left, int right) {
    int i, j, t, pivot;
    if (left > right)
        return;

    pivot = array[left]; // pivot中存的就是基准数
    i = left;
    j = right;
    while (i != j) {
        // 顺序很重要,要先从右边开始找
        while (array[j] >= pivot && i < j)
            j--;
        // 再找右边的
        while (array[i] <= pivot && i < j)
            i++;
        // 交换两个数在数组中的位置
        if (i < j) {
            t = array[i];
            array[i] = array[j];
            array[j] = t;
        }
    }
    // 最终将基准数归位
    array[left] = array[i];
    array[i] = pivot;

    quickSort(array, left, i - 1);//继续处理左边的,这里是一个递归的过程
    quickSort(array, i + 1, right);//继续处理右边的 ,这里是一个递归的过程
}

基数排序

概述

基数排序(Radix sort)是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

算法流程

通过基数排序对数组{53, 3, 542, 748, 14, 214, 154, 63, 616},它的示意图如下:

在上图中,首先将所有待比较树脂统一为统一位数长度,接着从最低位开始,依次进行排序。
1. 按照个位数进行排序。
2. 按照十位数进行排序。
3. 按照百位数进行排序。
排序后,数列就变成了一个有序序列。

算法效率

排序算法

平均时间复杂度

最佳情况

最坏情况

空间复杂度

排序方式

稳定性

基数排序

O(n*k)

O(n*k)

O(n*k)

O(n+k)

Out-place

稳定

代码实现

基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。

注意本次演示采用LSD的方式实现

最低位优先法,简称LSD法

public static void radixSort(int[] array) {
    int max = array[0];
    for (int i = 0; i < array.length; i++) {
        if (array[i] > max) {
            max = array[i];
        }
    }
    System.out.println("max:" + max);
    int maxDigit = 0;
    while (max != 0) {
        max = max / 10;
        maxDigit++;
    }
    System.out.println("maxDigit, " + maxDigit);
    int[][] buckets = new int[10][array.length];
    int base = 10;

    // 从低位到高位,对的每一位遍历,将所有元素分配到桶中
    for (int i = 0; i < maxDigit; i++) {
        int[] bucketLen = new int[100];    //存储各个桶中存储元素的数量

        // 收集:将不同桶里数据挨个捞出来,为下一轮高位排序做准备,由于靠近桶底的元素排名靠前,因此从桶底先捞
        for (int j = 0; j < array.length; j++) {
            int whichBucket = (array[j] % base) / (base / 10);
            buckets[whichBucket][bucketLen[whichBucket]] = array[j];
            bucketLen[whichBucket]++;
        }
        int k = 0;
        // 收集:将不同桶里数据挨个捞出来,为下一轮高位排序做准备
        for (int l = 0; l < buckets.length; l++) {
            for (int m = 0; m < bucketLen[l]; m++) {
                array[k++] = buckets[l][m];

            }
            base *= 10;
        }
    }
}

基数排序 vs 计数排序 vs 桶排序

这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:

基数排序:根据键值的每位数字来分配桶
计数排序:每个桶只存储单一键值
桶排序:每个桶存储一定范围的数值

计数排序和桶排序在这篇文章里具体就不写了

堆排序

看堆排序之前先介绍一下面几个概念:

完全二叉树: 若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树,很好理解如下图所示。

堆: 堆是具有以下性质的完全二叉树,每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:

同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子:

该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:

大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]

小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]

ok,了解了这些定义。接下来,我们来看看堆排序的基本思想及基本步骤:

基本思想

堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。

算法流程

步骤一 构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。

1.假设给定无序序列结构如下

2.此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整。

3.找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换。

这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。

此时,我们就将一个无需序列构造成了一个大顶堆。

步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。

b.重新调整结构,使其继续满足堆定义

 

c.再将堆顶元素8与末尾元素5进行交换,得到第二大元素8.

 

后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序

再简单 总结 下堆排序的基本思路:

a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;

b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;

c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。

算法效率

排序算法

平均时间复杂度

最佳情况

最坏情况

空间复杂度

排序方式

稳定性

堆排序

O(nlogn)

O(nlogn)

O(nlogn)

O(1)

In-place

不稳定

算法实现

public static void heapSort(int[] array) {
    // 1.构建大顶堆
    for (int i = array.length / 2 - 1; i >= 0; i--) {
        // 从第一个非叶子节从下至上,从左至右调整结构
        adjustHeap(array, i, array.length);
    }
    // 2.调整堆结构+交换堆顶元素与末尾元素
    for (int j = array.length - 1; j > 0; j--) {
        swap(array, 0, j);  // 将堆顶元素与末尾元素进行交换
        adjustHeap(array, 0, j);    // 重新对堆进行调整
    }
}

/**
 * 调整大顶堆(仅是调整过程,建立在大顶堆已构建的基础上)
 */
private static void adjustHeap(int[] array, int i, int length) {
    int temp = array[i];    // 先取出当前元素i
    for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {  // 从i节点的左子节点开始,也就是2i+1处开始
        if (k + 1 < length && array[k] < array[k + 1]) {  // 如果左子节点小于右子节点,k执行右子节点
            k++;
        }
        if (array[k] > temp) {  // 如果子节点大于父节点,将子节点赋值给父节点(不用进行交换)
            array[i] = array[k];
            i = k;
        } else {
            break;
        }
        array[i] = temp;    // 将temp值放到最终的位置
    }
}

总结

一、稳定性:

稳定:冒泡排序、插入排序、归并排序和基数排序

不稳定:选择排序、快速排序、希尔排序、堆排序

二、平均时间复杂度

O(n^2):直接插入排序,简单选择排序,冒泡排序。

在数据规模较小时(9W内),直接插入排序,简单选择排序差不多。当数据较大时,冒泡排序算法的时间代价最高。性能为O(n^2)的算法基本上是相邻元素进行比较,基本上都是稳定的。

O(nlogn):快速排序,归并排序,希尔排序,堆排序。

其中,快排是最好的, 其次是归并和希尔,堆排序在数据量很大时效果明显。

三、排序算法的选择

1.数据规模较小

(1)待排序列基本序的情况下,可以选择直接插入排序;

(2)对稳定性不作要求宜用简单选择排序,对稳定性有要求宜用插入或冒泡

2.数据规模不是很大

(1)完全可以用内存空间,序列杂乱无序,对稳定性没有要求,快速排序,此时要付出log(N)的额外空间。

(2)序列本身可能有序,对稳定性有要求,空间允许下,宜用归并排序

3.数据规模很大

(1)对稳定性有求,则可考虑归并排序。

(2)对稳定性没要求,宜用堆排序

4.序列初始基本有序(正序)

直接插入,冒泡

参考:

https://juejin.im/post/5b95da8a5188255c775d8124

https://zh.wikipedia.org/wiki/%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95

动画演示排序过程http://atool.org/sort.php

https://visualgo.net/zh/sorting

https://html-js.site/file/sortAnimate/index.html

https://www.toptal.com/developers/sorting-algorithms

http://www.webhek.com/post/comparison-sort.html

http://stevenshi.me/2018/07/23/algorithm/

https://itimetraveler.github.io/2017/07/18/八大排序算法总结与java实现/

https://juejin.im/post/59fbe7766fb9a0451c39bf21

https://www.cnblogs.com/chengxiao/category/880910.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值