排序基本上属于算法里面必须要掌握的一块了,也是各家面试的重点考察的部分之一。
所谓内部排序,就是参与排序的数据都存储在内存中。分析排序算法的性能,一般从算法的时间复杂度、空间复杂度和稳定性三个方面着手。为了方便对比分析,首先先把九大内部排序算法在时间、空间以及稳定性方面的性能总结如下:
分类 | 方法 | 时间复杂度 | 空间复杂度 | 稳定性 | ||
最好 | 最坏 | 平均 | ||||
交换排序 | 交换排序 | O(n) | O(![]() | O(![]() | O(1) | 稳定 |
冒泡排序 | O(n) | O(![]() | O(![]() | O(1) | 稳定 | |
快速排序 | O( | O(![]() | O(![]() | O(![]() | 不稳定 | |
插入排序 | 直接插入排序 | O(n) | O(![]() | O(![]() | O(1) | 稳定 |
希尔排序 | O(![]() | O( | O(1) | 不稳定 | ||
选择排序 | 简单选择排序 | O(![]() | O(![]() | O(![]() | O(1) | 不稳定 |
堆排序 | O(![]() | O(![]() | O(![]() | O(1) | 不稳定 | |
其他 | 归并排序 | O(![]() | O(![]() | O(![]() | O(n) | 稳定 |
计数排序 | O(d(r+n)) | O(d(r+n)) | O(d(r+n)) | O(r) | 稳定 |
交换排序
核心思想:根据序列中两条记录键值的比较结果,判断是否需要交换记录在序列中的位置。
其特点是将键值较大(或较小)的记录想序列的前端移动,将键值较小(或较大)的记录想序列的后端移动。
代码实现:
void sort(int arr[], int n) {
int i, j;
int k;
for (i = 0; i < n - 1; i++) {
for (j = i + 1; j < n; j++) {
if (arr[i] > arr[j]) {
k = arr[i];
arr[i] = arr[j];
arr[j] = k;
}
}
}
}
冒泡排序
核心思想:在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。
也就是说,每一轮冒泡,就找出了序列中未排序的最大(或最小)的数。
代码实现:
void sort(int arr[], int n) {
int i, j;
int k;
for (i = 0; i < n - 1; i++) {
for (j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
k = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = k;
}
}
}
}
冒泡排序优化
其实冒泡排序在每轮排序中,除了将最小(或最大)的数据放到靠后的位置之外,还使较小的数据放到靠近合适的位置。但是,如果在冒泡排序的过程中,已经获得了有序的序列,但是循环还没有结束,那么从这轮循环之后的循环都是无用功。如何避免这段时间的消耗呢?
其实,只需要在代码中设置一个简单的标志位即可。
代码实现:
void sort(int arr[], int n) {
int i, j;
int k, flag = 1; //设置标志位
for (i = 0; i < n - 1&&flag; i++) {
flag = 0;
for (j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
flag = 1;
k = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = k;
}
}
}
}
直接插入排序
核心思想:将一个记录插入到已排序好的有序表中,从而得到一个新、记录数增1的有序表。即:先将序列的第1个记录看成是一个有序的子序列,然后从第2个记录逐个进行插入,直至整个序列有序为止。
要点:设立哨兵,作为临时存储和判断数组边界之用。
代码实现:
void sort(int arr[], int n) {
int i, j;
int k;
for (i = 1; i < n; i++) {
k = arr[i];
for (j = i - 1; j >= 0 && arr[j]>k; j--) {
arr[j + 1] = arr[j];
}
arr[j + 1] = k;
}
}
直接插入排序的优化
在直接插入排序中,主要的时间消耗在数据的比较和移动上。那么有没有什么办法来降低数据比较的次数呢?
由于前半部分的序列已经有序,在为新数据寻找插入点时,可以采用折半查找的方法来提高寻找的速度。
代码实现:
void sort(int arr[], int n) {
int i, j;
int low, high, m;
int k;
for (i = 1; i < n; i++) {
k = arr[i];
low = 0;
high = i - 1;
while (low <= high) {
m = (low + high) / 2;
if (k < arr[m])
high = m - 1;
else
low = m + 1;
}
for (j = i - 1; j >= high + 1; j--) {
arr[j + 1] = arr[j];
}
arr[high + 1] = k;
}
}
简单选择排序
核心思想:在要排序的一组数中,选出最小(或者最大)的一个数与第1个位置的数交换;然后在剩下的数当中再找最小(或者最大)的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止。
代码实现:
void sort(int arr[], int n) {
int i, j, min;
int tmp;
for (i = 0; i < n; i++) {
min = i;
for (j = i; j < n; j++) {
if (arr[min] > arr[j])
min = j;
}
if (i != min) {
tmp = arr[i];
arr[i] = arr[min];
arr[min] = tmp;
}
}
}
归并排序
核心思想:归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
代码实现:
void Merge(int arr[], int tmp[], int start, int mid, int end) {
int i = start, j = mid + 1, k = start;
while (i != mid + 1 && j != end + 1) {
if (arr[i] >= arr[j])
tmp[k++] = arr[j++];
else
tmp[k++] = arr[i++];
}
while (i != mid + 1)
tmp[k++] = arr[i++];
while (j != end + 1)
tmp[k++] = arr[j++];
for (i = start; i <= end; i++)
arr[i] = tmp[i];
}
void sort(int arr[], int tmp[], int start, int end) {
int mid;
if (start < end) {
mid = (start + end) / 2;
sort(arr, tmp, start, mid);
sort(arr, tmp, mid + 1, end);
Merge(arr, tmp, start, mid, end);
}
}
快速排序
核心思想:
- 选择一个基准元素,通常选择第一个元素或者最后一个元素;
- 通过一趟排序讲待排序的记录分割成独立的两部分,其中一部分记录的元素值均比基准元素值小。另一部分记录的 元素值比基准值大;
- 此时基准元素在其排好序后的正确位置;
- 然后分别对这两部分记录用同样的方法继续进行排序,直到整个序列有序。
代码实现:
void sort(int arr[], int l, int r) {
if (l >= r) {
return;
}
int i = l, j = r, x = arr[l];
while (i < j) {
while (i < j&&arr[j] >= x)
j--;
arr[i] = arr[j];
while (i < j&&arr[i] <= x)
i++;
arr[j] = arr[i];
}
arr[i] = x;
sort(arr, l, i - 1);
sort(arr, i + 1, r);
}
希尔排序
核心思想:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
代码实现:
void sort(int arr[], int n) {
int i, j, d;
int tmp;
d = n / 2;
while (d > 0) {
for (i = d; i < n; i++) {
tmp = arr[i];
j = i - d;
while (j >= 0 && tmp < arr[j]) {
arr[j + d] = arr[j];
j = j - d;
}
arr[j + d] = tmp;
}
d = d / 2;
}
}
堆排序
核心思想:从算法描述来看,堆排序需要两个过程,一是建立堆,二是堆顶与堆的最后一个元素交换位置。所以堆排序有两个函数组成。一是建堆的渗透函数,二是反复调用渗透函数实现排序的函数。
建立堆的过程,对每个非叶子节点进行渗透函数的调用;渗透函数的过程,非叶子节点与其子节点中的大者(或小者)比大小,若大于(或小于),则进行交换。
代码实现:
void HeapAdjust(int arr[], int i, int n) {
int nChild;
int tmp;
for (; 2 * i + 1 < n; i = nChild) { //判断是否为叶子结点,当调整了一个,下面的都要调整
nChild = 2 * i + 1;
if (nChild < n - 1 && arr[nChild + 1] > arr[nChild]) //判断右结点是否存在,取大值
++nChild;
if (arr[i] < arr[nChild]) {
tmp = arr[i];
arr[i] = arr[nChild];
arr[nChild] = tmp;
}
else
break;
}
}
void sort(int arr[], int n) {
int i;
int tmp;
for (i = (n - 1) / 2; i >= 0; i--)
HeapAdjust(arr, i, n); //对序列中的每个非叶子节点执行调整算法,使之成为一个堆
for (i = n - 1; i > 0; i--) { //从最后一个元素开始对序列进行调整,直到第一个元素
tmp = arr[0];
arr[0] = arr[i];
arr[i] = tmp;
HeapAdjust(arr, 0, i);
}
}