时间复杂度 O(n^2) 级排序算法
冒泡排序
快速排序
插入排序
冒泡排序
冒泡排序有三种写法:
- 初级写法:一边比较一边向后两两交换,将最大值 或 最小值冒泡到最后一位;
- 经过优化的写法:使用一个变量记录当前轮次的比较是否发生过交换,如果没有发生交换表示已经有序,不再继续排序;
- 进一步优化的写法:除了使用变量记录当前轮次是否发生交换外,再使用一个变量记录上次发生交换的位置,下一轮排序时到达上次交换的位置就停止比较。
冒泡排序的第一种写法:基础写法
最外层的 for 循环每经过一轮,剩余数字中的最大值就会被移动到当前轮次的最后一位,中途也会有一些相邻的数字经过交换变得有序。总共比较次数是(n-1)+(n-2)+(n-3)+…+1。
这种写法相当于相邻的数字两两比较,并且规定:“谁大谁站右边”。经过 n-1 轮,数字就从小到大排序完成了。整个过程看起来就像一个个气泡不断上浮,这也是“冒泡排序法”名字的由来。
其中,我们在交换两个数字时使用了一个小魔术:
没有引入第三个中间变量就完成了两个数字的交换。
除了这种先加后减的写法,还有一种先减后加的写法。
public static void bubbleSort(int[] arr)
{
for (int i = 0; i < arr.Length - 1; i++) //代表轮次循环
{
for (int j = 0; j < arr.Length - i - 1; j++) //代表组内循环
{
if (arr[j] > arr[j + 1]) //如果左边的数大于右边的数,且不使用中间变量进行交换
{
arr[j + 1] = arr[j + 1] + arr[j];
arr[j] = arr[j + 1] - arr[j]; //arr[j] 和 arr[j+1]进行了交换
arr[j + 1] = arr[j + 1] - arr[j];
}
}
}
}
冒泡排序的第二种写法:添加排序条件
最外层的 for 循环每经过一轮,剩余数字中的最大值仍然是被移动到当前轮次的最后一位。
这种写法相对于第一种写法的优点是:如果一轮比较中没有发生过交换,则立即停止排序,因为此时剩余数字一定已经有序了。
添加一个排序启动条件
看下动图演示:
动图中可以看出:
第一轮排序将数字 6 移动到最右边;
第二轮排序将数字 5 移动到最右边,同时中途将 1 和 2 排了序;
第三轮排序时,没有发生交换,表明排序已经完成,不再继续比较。
public static void bubbleSort_2(int[] arr)
{
bool is_Swapped = true; //判断排序是否启动的条件
for (int i = 0; i < arr.Length - 1; i++) //代表轮次循环
{
if (!is_Swapped) break; //如果没有发生交换,说明剩余部分已经有序
is_Swapped = false; //先设置为false
for (int j = 0; j < arr.Length - i - 1; j++) //代表组内循环
{
if (arr[j] > arr[j + 1]) //如果左边的数大于右边的数,且不使用中间变量进行交换
{
arr[j + 1] = arr[j + 1] + arr[j];
arr[j] = arr[j + 1] - arr[j]; //arr[j] 和 arr[j+1]进行了交换
arr[j + 1] = arr[j + 1] - arr[j];
is_Swapped = true; //发生了交换,设置启动条件
}
}
}
}
冒泡排序的第三种写法:添加排序条件+上次交换排序后位置
1.最外层的 while 循环每经过一轮,剩余数字中的最大值MaxValue会被移动到当前轮次的最后一位。
2.在下一轮比较时,只需比较到上一轮比较中,获取到最后比较位置的索引。因为后面的所有元素都没有发生过交换,必然已经有序了。
3.如果当一轮比较中从头到尾都没有发生过交换,则表示整个列表已经有序,排序完成。
public static void bubbleSort_3(int[] arr)
{
//优化:添加一个上次交换的索引
bool is_Swapped = true;
int lastSortIndex = arr.Length - 1;
int swappedIndex=-1;
while (is_Swapped) //控制外圈循环
{
is_Swapped = false;
for (int i = 0; i < lastSortIndex; i++) //控制组内循环
{
if (arr[i] > arr[i + 1]) //如果左边的数大于右边的数,且不使用中间变量进行交换
{
arr[i + 1] = arr[i + 1] + arr[i];
arr[i] = arr[i + 1] - arr[i]; //arr[j] 和 arr[j+1]进行了交换
arr[i + 1] = arr[i + 1] - arr[i];
is_Swapped = true; //发生了交换,设置启动条件
swappedIndex = i; //交换的最后一次索引位置
}
}
lastSortIndex = swappedIndex; // 最后一个没有经过排序的元素的下标就是最后一次发生交换的位置,控制组内循环结束位置
}
}
冒泡排序的 时间复杂度 & 空间复杂度
冒泡排序(一)它的空间复杂度为 O(1),时间复杂度为 O(n^2)
第二种、第三种冒泡排序由于经过优化,最好的情况下只需要 O(n)的时间复杂度。
最好情况:在数组已经有序的情况下,只需遍历一次,由于没有发生交换,排序结束。
最差情况:数组顺序为逆序,每次比较都会发生交换。
但优化后的冒泡排序平均时间复杂度仍然是 O(n^2)
所以这些优化对算法的性能并没有质的提升。
正如 Donald E. Knuth(1974 年图灵奖获得者)所言:“冒泡排序法除了它迷人的名字和导致了某些有趣的理论问题这一事实外,似乎没有什么值得推荐的。”
不管怎么说,冒泡排序法是所有排序算法的老祖宗,如同程序界经典的 “Hello, world” 一般经久不衰,总是出现在各类算法书刊的首个章节。但面试时如果你说你只会冒泡排序可就太掉价了,下一节我们就来认识一下他的继承者们。
选择排序
选择排序的第一种写法【不稳定,破坏顺序结构】
选择排序的思想是:双重循环遍历数组,每经过一轮比较,找到最小元素的下标,将其交换至首位。
public static void selectionSort_1(int[] arr)
{
int minIndex;//需要一个最小值索引下标
for (int i = 0; i < arr.Length-1; i++)
{
minIndex = i; //每一个位置的最小值从头开始
for (int j = i+1; j < arr.Length; j++)
{
if (arr[minIndex]>arr[j]) //当前最小值位置 > J位置
{
minIndex = j;//记录最新的最小值位置,循环结束后最后得出最小值位置
}
}
//交换当前位置和最小值位置
arr[minIndex] = arr[minIndex] + arr[i];
arr[i] = arr[minIndex] - arr[i]; //现在当前位置变为最小值
arr[minIndex] = arr[minIndex] - arr[i]; //把原先的最小值位置交换到前面
}
}
动图演示
冒泡排序和选择排序有什么异同?
相同点:
- 都是两层循环,时间复杂度都为 O(n^2)
- 都只使用有限个变量,空间复杂度 O(1)
不同点:
- 冒泡排序在比较过程中就不断交换;而选择排序增加了一个变量保存最小值 / 最大值的下标,遍历完成后才交换,减少了交换次数。
- 最重要的不同点:冒泡排序法是稳定的,选择排序法是不稳定的。
【不稳定的例子】
选择排序中,最小值和首位交换的过程可能会破坏稳定性。比如数列:[2, 2, 1],在选择排序中第一次进行交换时,原数列中的两个 2 的相对顺序就被改变了,因此,我们说选择排序是不稳定的。
选择排序的第二种写法:二元选择排序(同时找出最大和最小值)
使用二元选择排序,每轮选择时记录最小值和最大值,可以把数组需要遍历的范围缩小一倍。
public static void selectionSort_2(int[] arr)
{
int minIndex, maxIndex;//需要一个最小值索引下标,和最大值索引下标
for (int i = 0; i < arr.Length / 2; i++) //循环范围减少一半
{
minIndex = maxIndex = i; //每一个位置的最小值最大值从头开始
for (int j = i + 1; j < arr.Length; j++)
{
if (arr[minIndex] > arr[j]) //当前最小值位置 > J位置
{
minIndex = j;//记录最新的最小值位置,循环结束后最后得出最小值位置
}
if (arr[maxIndex] < arr[j]) //当前最大值位置 < J位置
{
minIndex = j;//记录最新的最小值位置,循环结束后最后得出最小值位置
}
}
if (maxIndex == minIndex) break; //如果最大值和最小值下标是相同的,说明已经排好序了
arr[minIndex] = arr[minIndex] + arr[i];//交换当前位置和最小值位置
arr[i] = arr[minIndex] - arr[i]; //现在当前位置变为最小值
arr[minIndex] = arr[minIndex] - arr[i]; //把原先的最小值位置交换到前面
//最重要的代码,它位于交换最小值和交换最大值的代码中间
if (maxIndex == i) maxIndex = minIndex; //I已经和minIndex交换过位置,如果maxIndex和I相同,就进行和MinIndex
int lastIndex = arr.Length - i - 1; //把最大值交换到最后面
arr[maxIndex] = arr[maxIndex] + arr[lastIndex];
arr[lastIndex] = arr[maxIndex] - arr[lastIndex];
arr[maxIndex] = arr[maxIndex] - arr[lastIndex];
}
}
二元选择排序中有一句很重要的代码,它位于交换最小值和交换最大值的代码中间
二元选择排序和经典选择排序效率对比
选择排序 的 时间复杂度 & 空间复杂度
前文已经说到,选择排序使用两层循环,时间复杂度为 O(n^2)
只使用有限个变量,空间复杂度 O(1)
二元选择排序虽然比选择排序要快,但治标不治本,二元选择排序中做的优化无法改变其时间复杂度。
二元选择排序的时间复杂度仍然是 O(n^2)
只使用有限个变量,空间复杂度 O(1)
插入排序
插入排序的第一种写法
当数字少于两个时,不存在排序问题,当然也不需要插入,所以我们直接从第二个数字开始往前插入。将当前的数字不断与前面的数字比较,寻找插入位置。
在比较的过程中,我们将大于当前数字的元素不断向后移动。这个过程就像是已经有一些数字坐成了一排,这时一个新的数字要加入,所以这一排数字不断地向后腾出位置,当新的数字找到自己合适的位置后,就可以直接坐下了。重复此过程,直到排序结束。
动图演示
插入数据时,不仅可以用这种移动元素的方式,还可以使用交换元素的方式!
public static void insertSort_1(int[] arr)
{
for (int i = 1; i < arr.Length; i++) //循环,从第二个开始,往前插入数值
{
int curIdx = i; //记录当前索引值
while (curIdx>=1&& arr[curIdx]<arr[curIdx-1]) //当前数值比之前小,就交换,一直循环,前面完全排序
{
arr[curIdx] = arr[curIdx] + arr[curIdx - 1];
arr[curIdx - 1] = arr[curIdx] - arr[curIdx - 1];
arr[curIdx] = arr[curIdx] - arr[curIdx - 1];
curIdx--; //更新当前索引值,向前一位
}
}
}
建议可以使用2个for循环~~,冒泡,选择也使用了2个for循环。
public static void InsertSort_2(int[] arr)
{
for (int i = 0; i < arr.Length; i++)
{
for (int j = i; j > 0 && arr[j] < arr[j - 1]; j--) // 比较当前数字和前一个数字,
{
int temp = arr[j];
arr[j] = arr[j - 1];
arr[j - 1] = temp;
}
}
}
插入排序的 时间复杂度 & 空间复杂度
插入排序过程需要两层循环,时间复杂度为 O(n^2)
只需要常量级的临时变量,空间复杂度为 O(1)
时间复杂度 O(nlogn) 级排序算法
希尔排序
希尔排序本质上是对插入排序的一种优化,它利用了插入排序的简单,又克服了插入排序每次只交换相邻两个元素的缺点。它的基本思想是:
将待排序数组按照一定的间隔分为多个子数组,每组分别进行插入排序。
这里按照间隔分组指的不是取连续的一段数组,而是每跳跃一定间隔取一个值组成一组。
逐渐缩小间隔进行下一轮排序
最后一轮时,取间隔为 1,也就相当于直接使用插入排序。但这时经过前面的“宏观调控”,数组已经基本有序了,所以此时的插入排序只需进行少量交换便可完成。
5间隔排序,2间隔排序,1间隔排序
消除逆序队,突破时间复杂度N(O^2)
堆排序
堆:符合以下两个条件之一的完全二叉树:
根节点的值 ≥ 子节点的值,这样的堆被称之为最大堆,或大顶堆;
根节点的值 ≤ 子节点的值,这样的堆被称之为最小堆,或小顶堆。
堆排序过程如下:
用数列构建出一个大顶堆,取出堆顶的数字;
调整剩余的数字,构建出新的大顶堆,再次取出堆顶的数字;
循环往复,完成整个排序。
推排序的第一种写法:大顶堆
整体的思路就是这么简单,我们需要解决的问题有两个:
如何用数列构建出一个大顶堆,对应函数BuildMaxHeap;
取出堆顶的数字后,如何将剩余的数字调整成新的大顶堆,调整过程由 MaxHeapify 函数处理。
构建大顶堆 & 调整堆
构建大顶堆有两种方式:
方案一:从 0 开始,将每个数字依次插入堆中,一边插入,一边调整堆的结构,使其满足大顶堆的要求;
方案二:将整个数列的初始状态视作一棵完全二叉树,自底向上调整树的结构,使其满足大顶堆的要求。
方案二更为常用,动图演示如下:
在介绍堆排序具体实现之前,我们先要了解完全二叉树的几个性质。将根节点的下标视为 0,则完全二叉树有如下性质:
对于完全二叉树中的第 i 个数,它的左子节点下标:left = 2i + 1
对于完全二叉树中的第 i 个数,它的右子节点下标:right = left + 1
对于有 n 个元素的完全二叉树(n >2),它的最后一个非叶子结点的下标:n/2 - 1
public static void heapSort(int[] arr)
{
// 构建初始大顶堆
BuildMaxHeap(arr);
for (int i = arr.length - 1; i > 0; i--) {
// 将最大值放到数组最后,堆顶元素与无序区最后一位交换(使堆顶元素进入有序区)
Swap(arr, 0, i);
// 调整剩余数组,使其满足大顶堆,重新将无序区调整为大顶堆
MaxHeapify(arr, 0, i);
}
}
//构造大顶堆
private void BuildMaxHeap(int[] arr)
{
for (int i = arr.Length / 2 - 1; i >= 0; i--) //根据大顶堆的性质,前半段元素为root节点,后半段元素都是叶子节点
{
MaxHeapify(arr, i, arr.Length); //调整大顶堆,从最底层的最后一个root节点开始
}
}
//调整大顶堆
private void MaxHeapify(int[] arr, int curIdx, int heapLength)
{
int left = 2 * curIdx + 1;//左叶子的位置 这是对应关系 0 1 2 , 1 3 4, 2 5 6 ,3 7 8
int right = 2 * curIdx + 2;//左叶子的位置
int MaxIdx = curIdx; //记录此根节点、左右节点
if (left < heapLength && arr[left] > arr[MaxIdx]) //更节点与左节点比较
{
MaxIdx = left;
}
if (right < heapLength && arr[right] > arr[MaxIdx]) //更节点与右节点比较
{
MaxIdx = right;
}
if (curIdx != MaxIdx) //左右节点有大于更节点的情况
{
Swap(arr, MaxIdx, curIdx); //交换数值
MaxHeapify(arr, MaxIdx, heapLength);//继续递归调整,确认更节点左右节点符合规则
}
}
//交换树脂
private void Swap(int[] arr, int a, int b)
{
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
堆排序的 时间复杂度 & 空间复杂度
堆排序分为两个阶段:初始化建堆(buildMaxHeap)和重建堆(maxHeapify,直译为大顶堆化)。所以时间复杂度要从这两个方面分析。
根据数学运算可以推导出初始化建堆的时间复杂度为 O(n),重建堆的时间复杂度为 O(nlogn),故堆排序总的时间复杂度为 O(nlogn)。推导过程较为复杂,故不再给出证明过程。
堆排序的空间复杂度为 O(1),只需要常数级的临时变量。
堆排序是一个优秀的排序算法,但是在实际应用中,快速排序的性能一般会优于堆排序,我们将在下一节介绍快速排序的思想。
快速排序(效率最高)
它的时间复杂度也是 O(nlogn),但它在时间复杂度为 O(nlogn) 级的几种排序算法中,大多数情况下效率更高,所以快速排序的应用非常广泛。
加上快速排序所采用的[分治思想]非常实用,使得快速排序深受面试官的青睐,所以掌握快速排序的思想尤为重要。
快速排序算法的基本思想是:
1、从数组中取出一个数,称之为基数(pivot)
2、遍历数组,将比基数大的数字放到它的右边,比基数小的数字放到它的左边。遍历完成后,数组被分成了左右两个区域
3、将左右两个区域视为两个数组,重复前两个步骤,直到排序完成
事实上,快速排序的每一次遍历,都将基数摆到了最终位置上。
动图演示
快速排序的框架
//快速排序框架
public static void QuickSort(int[] arr)
{
QuickSort(arr, 0, arr.Length - 1);
}
public static void QuickSort(int[] arr, int start, int end)
{
// 将数组分区,并获得中间值的下标
int middle = Partition(arr, start, end);
// 对左边区域快速排序
QuickSort(arr, start, middle - 1);
// 对右边区域快速排序
QuickSort(arr, middle + 1, end);
}
public static int Partition(int[] arr, int start, int end)
{
// TODO: 将 arr 从 start 到 end 分区,左边区域比基数小,右边区域比基数大,然后返回中间值的下标
return 0;
}
退出递归的边界条件
很容易想到,当某个区域只剩下一个数字的时候,自然不需要排序了,此时退出递归函数。实际上还有一种情况,就是某个区域只剩下 0 个数字时,也需要退出递归函数。当 middle 等于 start 或者 end 时,就会出现某个区域剩余数字为 0。
所以判断剩余区域的数字为 0 个或者 1 个也就是指 start 或 end 与 middle 相等或相差 1。
我们来分析一下这四个判断条件:
当 start == middle 时,相当于 quickSort(arr, start, middle - 1) 中的 start == end + 1
当 start == middle - 1 时,相当于 quickSort(arr, start, middle - 1) 中的 start == end
当 middle == end 时,相当于 quickSort(arr, middle + 1, end) 中的 start == end + 1
当 middle == end -1 时,相当于 quickSort(arr, middle + 1, end) 中的 start == end
这样我们就写出了最简洁版的边界条件,我们需要知道,这里的 start >= end 实际上只有两种情况:
start == end: 表明区域内只有一个数字
start == end + 1: 表明区域内一个数字也没有
基数的选择
基数的选择没有固定标准,随意选择区间内任何一个数字做基数都可以。通常来讲有三种选择方式:
1、选择第一个元素作为基数
2、选择最后一个元素作为基数
3、选择区间内一个随机元素作为基数(时间复杂度是最优解)
快速排序 双指针分区算法
还有一种双指针的分区算法更为常用:从 left 开始,遇到比基数大的数,记录其下标;再从 right 往前遍历,找到第一个比基数小的数,记录其下标;然后交换这两个数。继续遍历,直到 left 和 right 相遇。然后就和刚才的算法一样了,交换基数和中间值,并返回中间值的下标。
快速排序的优化思路
第一种就是我们在前文中提到的,每轮选择基数时,从剩余的数组中随机选择一个数字作为基数。这样每轮都选到最大或最小值的概率就会变得很低了。所以我们才说用这种方式选择基数,其平均时间复杂度是最优的
第二种解决方案是在排序之前,先用洗牌算法将数组的原有顺序打乱,以防止原数组正序或逆序。
public static void QuickSort(int[] arr)
{
QuickSort(arr, 0, arr.Length - 1);
}
public static void QuickSort(int[] arr, int start, int end)
{
// 如果区域内的数字少于 2 个,退出递归
if (start >= end) return;
// 将数组分区,并获得中间值的下标
int middle = Partition(arr, start, end);
// 对左边区域快速排序
QuickSort(arr, start, middle - 1);
// 对右边区域快速排序
QuickSort(arr, middle + 1, end);
}
// 将 arr 从 start 到 end 分区,左边区域比基数小,右边区域比基数大,然后返回中间值的下标
public static int Partition(int[] arr, int start, int end)
{
int pivot = arr[start]; // 取第一个数为基数
int left = start + 1; // 左边界,从第二个数开始分区
int right = end; // 右边界
while (left < right)
{
while (left < right && arr[left] <= pivot) left++; // 找到第一个大于基数的位置
while (left < right && arr[right] >= pivot) right--; // 找到第一个小于基数的位置
// 交换这两个指针,使得左边分区都小于或等于基数,右边分区大于或等于基数
if (left < right)
{
exchange(arr, left, right);
left++;
right--;
}
}
// 如果 left 和 right 相等,单独比较 arr[right] 和 pivot
if (left == right && arr[right] > pivot) right--;
// 将基数和轴交换
exchange(arr, start, right);
return right;
}
private static void exchange(int[] arr, int i, int j)
{
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
//三数取中法,另一种分区算法
private void Partition(int[] nums, int start, int end, int k)
{
if (start >= end) return;
int middle = start + (end - start) / 2; //中间索引值
SwapIfGreater(nums, start, end); //交换最大值在右边,最小值在左边
SwapIfGreater(nums, middle, start); //交换
SwapIfGreater(nums, middle, end);
int pivot = nums[start]; //基数数值选择第一个
int i = start;
int j = end;
while (i < j)
{
while (i < j && nums[j] >= pivot){ --j;}
while (i < j && nums[i] <= pivot){ ++i;}
if (i < j) { Swap(nums, i, j); }
}
Swap(nums, start, i);
if (i > k) { Partition(nums, start, i - 1, k); }
else if (i < k) { Partition(nums, i + 1, end, k);}
}
private void SwapIfGreater(int[] nums, int i, int j)
{
if (i != j)
{
if (nums[i] > nums[j])
{
Swap(nums, i, j);
}
}
}
private void Swap(int[] nums, int i, int j)
{
if (i != j)
{
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
}
快速排序的 时间复杂度 & 空间复杂度
快速排序的时间复杂度,平均时间复杂度为 O(nlogn),最坏的时间复杂度为 O(n^2)
空间复杂度与递归的层数有关,每层递归会生成一些临时变量,所以空间复杂度为 O(logn)~ O(n),平均空间复杂度为 O(logn)。
空间换时间~
归并排序(性能较好,稳定)
在了解归并排序之前,我们先一起思考一个问题:如何将两个有序的列表合并成一个有序的列表?
将数组拆分成有序数组
拆分过程使用了二分的思想,这是一个递归的过程
归并排序的优化:减少临时空间的开辟
为了减少在递归过程中不断开辟空间的问题,我们可以在归并排序之前,先开辟出一个临时空间,在递归过程中统一使用此空间进行归并即可。
归并排序的第一种写法:开辟新空间拆分有序数组
//归并排序,需要额外空间result
public static void MergeSort(int[] array)
{
if (array.Length == 0) return;
int[] result = new int[array.Length];
MergeSort(array, 0, array.Length - 1, result);
}
//1、划分左右区域
private static void MergeSort(int[] arr, int start, int end, int[] result)
{
int middle = start + (end - start) / 2;
MergeSort(arr, start, middle, result); //划分左边区域
MergeSort(arr, middle + 1, end, result); //划分右边区域
Merge(arr, start, end, result); //归并左右区域
}
//2、归并左右区域
private static void Merge(int[] arr, int start, int end, int[] result)
{
int start1 = start;
int end1 = (end + start) / 2;
int start2 = end1 + 1;
int end2 = end;
//遍历左右数组的指针
int idx1 = start;
int idx2 = start2;
while (idx1<=end1 && idx2 <=end2) //一直循环,直到result数组填充完毕
{
if (arr[idx1] <= arr[idx2]) //往result数组里添加数据
result[idx1 + idx2 - start2] = arr[idx1++]; //先赋值,在自增
else
result[idx1 + idx2 - start2] = arr[idx2++]; //先赋值,在自增
}
while (idx1<=end1) //将剩余的数字补到结果数组里
{
result[idx1 + idx2 - start2] = arr[idx1++]; //先赋值,在自增
}
while (idx2 <= end2) //将剩余的数字补到结果数组里
{
result[idx1 + idx2 - start2] = arr[idx2++]; //先赋值,在自增
}
while (start<=end) //将结果数组copy到arry中,以便下次比较
{
arr[start] = result[start];
start++;
}
归并排序的第二种写法:原地归并排序
Leetcode相关练习题
1.剑指 Offer 40题 最小的 k 个数 (这道题推荐用【大顶堆】堆排序做~~)
2. 283题 移动零 (这里推荐双指针法,数据交换)
3. 912题 排序数组
4. 面试题 10.01. 合并排序的数组
5. 147题 对链表进行插入排序
6. 912题 排序数组
7. 23题 合并 K 个升序链表
8. 剑指 Offer 25. 合并两个排序的链表
9. 剑指 Offer 51. 数组中的逆序对
总结
注意,如果原本的序列就是有序(升序或降序)的,快排基本就是和冒泡是兄弟了,慢到让你怀疑人生。希尔、归并、堆排、基数基本影响不大。可见如果数据本来就只有微小的无序,还是不要用快排了。
8种排序的稳定性
不稳定的排序算法 | 稳定的排序算法 |
---|---|
选择排序 | 冒泡排序 |
希尔排序 | 插入排序 |
快速排序 | 归并排序 |
堆排序 | 基数排序 |
作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/sort-algorithms/evsqoi/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。