排序
预备知识
稳定性:对于任意两个相同关键字的元素,若执行某个排序算法后其相对位置没有发生改变(即本来在前面的元素还在前面),则称该排序算法是稳定的。
内部排序和外部排序:根据数据元素是否完全在内存中,分为内部排 序和外部排序两种。
一般情况下内部排序算法都需要通过比较和移动进行排序,但是也有例外,如 基数排序。

插入排序
基本思想是将待排序的元素插入到已经排序好的子序列中,可以引申出三种重要的排序算法—— 直接插入排序、折半插入排序 和 希尔排序j.
直接插入排序
步骤如下:
- 初始时将第一个元素作为已经排好的子列;
- 对未排好的元素寻找其在已排好子列的位置,同时将所有大于/小于该元素的元素向后排;
代码如下:
void inserSort(int *arr, int len) {
int i, j, key;
for (int i = 1; i < len; i++) {
key = arr[i]; // 使用一个变量存储元素
j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j]; // 向后移动
j--;
}
arr[j + 1] = key;
}
}
空间复杂度:
时间复杂度:最好的情况只需要从头到尾遍历一次,时间复杂度为 ,最坏情况为逆序,总的时间复杂度为 ,平均情况取 .
折半插入排序
由于直接插入排序中已经有一部分元素有序的,因此不再需要遍历寻找,可以使用折半查找,代码如下
void binaryInsertionSort(int *arr, int len) {
int j, key, left, right;
for (int i = 1; i < len; i++) {
key = arr[i];
left = 0;
right = i - 1;
while (left <= right) {
j = (left + right) / 2;
if (key < arr[j])
right = j - 1;
else // 将相同的元素插入后面,折半插入排序是稳定排序
left = j + 1;
}
// 先移动,再互换
for (j = i - 1; j >= left; --j)
arr[j + 1] = arr[j];
arr[left] = key;
}
}
减少了比较元素的次数,时间复杂度约为 ,但是移动元素的次数并没有改变,因此折半插入排序的时间复杂度仍然为 ;但是对于数据量不大的排序表,往往能表现出很好的性能。
希尔排序
前两种排序在基本有序和数据不大的情况下性能较好,希尔排序正是基于这两点分析对直接插入排序进行改进而来,又称为 最小增量排序;其基本思想是先将待排序列分割为若干相同长度的子列,这些子列是相隔某个 增量 的元素组成的,对各个子列分别进行直接插入排序,不断减小 增量,使得整个序列基本有序,再对整体进行插入排序(即增量为 的情况)。
代码如下,只是在直接插入代码的基础上额外嵌套了一层循环,并将其中的 都修改为了 ,下面蓝色部分为直接插入排序的代码。
void shellSort(int *arr, int len) {
int j, key;
for (int d = len / 2; d >= 1; d /= 2) {
for (int i = d; i < len; i++) {
key = arr[i];
j = i - d;
while (j >= 0 && arr[j] > key) {
arr[j + d] = arr[j];
j -= d;
}
arr[j + d] = key;
}
}
}
当 在某个特定的范围时,希尔排序的时间复杂度是 ,最坏情况下 (应该还是逆序输入)的时间复杂度是
希尔排序是不稳定的,因为相同的关键字元素可能会被划分到不同的子表中,从而导致了相对次序变化。
交换排序
交换指的对序列中的两个元素进行交换,常用的算法有冒泡算法和快速排序。
冒泡排序
冒牌排序这个名称十分形象,指的是每次排序将最小元素(最大元素)放到序列的最后 (或者最前),就像泡泡一样。
代码如下,其中 表示已经排序好子列的索引。
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
void bubbleSort(int *arr, int len) {
for (int i = 0; i < len - 1; i++) {
for (int j = len - 1; j > i; j--) {
if (arr[j - 1] > arr[j]) {
swap(&arr[j - 1], &arr[j]);
}
}
}
}
冒泡排序可以进一步优化——若在一次遍历当中,没有元素被交换,则说明已经有序了,直接返回即可;代码如下:
void imporvedBubbleSort(int *arr, int len) {
for (int i = 0; i < len - 1; i++) {
bool flag = true;
for (int j = len - 1; j > i; j--) {
if (arr[j - 1] > arr[j]) {
swap(&arr[j - 1], &arr[j]);
flag = false;
}
}
if (flag)
return;
}
}
当序列有序时,时间复杂度为 ,逆序时,每次交换元素需要 次移动,比较次数为 ,移动次数为 ,则最坏时间复杂度为 .
由于相同时并不会发生交换,则是稳定排序。
快速排序
快速排序的思想是基于分治,在待排序列中选择一个元素作为基准元素,将所有小于其的元素都放入其左边,大于其的元素都放入其右边,则其最终会被分配到正确的位置上,再通过递归的形式对左右两个部分进行相同的操作,直到所有元素有序。
代码实现上,一般是由头尾两个索引表示区间的长度,基准元素就选择区间中的第一个元素,初始情况,尾索引向头移动,直到遇到某个元素小于基准元素,则将其和基准元素交换;头索引和尾索引的移动是交替的,因为要保证一定有一个索引指向基准元素;当一次划分完成后,基准元素到达了正确的位置,则以其作为分界点,对左右两个部分进行相同的操作即可。
代码如下:
int partition(int *arr, int left, int right) {
int key = arr[left];
while (left < right) {
while (left < right && arr[right] >= key)
right--;
arr[left] = arr[right];
while (left < right && arr[left] <= key)
left++;
arr[right] = arr[left];
}
arr[left] = key;
return left;
}
void quickSort(int *arr, int left, int right) {
if (left < right) {
int split_index = partition(arr, left, right);
quickSort(arr, left, split_index - 1);
quickSort(arr, split_index + 1, right);
}
}
如何使用快速排序求出某个序列的第 大的元素?
int topkth(int *arr, int left, int right, int k) {
if (left < right) {
int split_index = partition(arr, left, right);
if (split_index == k)
return arr[split_index];
else if (split_index > k)
return topkth(arr, left, split_index - 1, k);
else if (split_index < k)
return topkth(arr, split_index + 1, right, k);
}
return -1;
}
空间效率:由于快速排序是递归的,因此需要一个递归工作栈来保存调用信息,其容量和递归调用的最大深度一致。 最好的情况下为 ,最坏情况下为 ,因此平均为 .
时间效率:与划分是否对称有关,最坏的情况发生在两个区域分别包含 和 个元素,并且发生在每一层上,则时间复杂度为 .
有许多方式可以提高算法的效率,一种方式是尽量选取一个可以将数据中分的元素,如选取序列头尾和中间三个元素的中间值,或者随机选取。
在最理想的情况下,即 partition 可以做到最平均的划分,速度将大大提升;但是其平均情况下的运行时间和最佳情况很接近,是所有内部排序算法中平均性能最优的算法。
快速排序的非递归实现
大致思路是借助栈来存储每次 partition 之后的结果,并将每次排序的左右坐标存入栈中,再进行排序直到栈为空。