排序算法总结
基本概念
- 定义:排序是将一批无序的记录(数据)重新排列成按关键字有序的记录序列的过程。
- 分类:
- 内部排序:待排序的记录数不太多,所有的记录都能存放在内存中进行排序。
- 外部排序:待排序的记录数太多,所有的记录不可能存放在内存中, 排序过程中必须在内、外存之间进行数据交换。
- 稳定性:
- 稳定排序:对于关键字相等的记录,排序前后相对位置不变。
- 不稳定排序:对于关键字相等的记录,排序前后相对位置可能发生变化。
交换排序类
冒泡排序
参考文章:http://c.biancheng.net/view/3444.html
- 原理:两两相邻的关键字,如果反序则交换
- 图解:对下面的无序表进行升序排序
- C语言代码:
void BubbleSort(int a[], int n)
{
int i, j, flag=1, temp;
for(i = n-1; i > 0 && flag == 1; i--)
{
flag = 0;
for(j = 1; j <= i; j++)
if(a[j] > a[j+1])
{
flag = 1;
temp = a[j+1]; // 交换
a[j+1] = a[j];
a[j] = temp;
}
}
}
void BubbleSort(int arr[], int n)
{
int i, j, flag, temp;
for(i = 0; i < n-1; i++) // 外层循环控制冒泡排序的轮数,n-1轮
{
flag = 0;
for(j = 0;j < n-1-i; j++) // 内层循环控制每轮冒泡排序比较的次数
if(arr[j] > arr[j+1])
{
flag = 1;
temp = arr[j+1];
arr[j+1] = arr[j];
arr[j] = temp;
}
if(flag == 0) break; // 表中记录排序完成
}
}
快速排序
参考文章:https://www.cnblogs.com/skywang12345/p/3596746.html
挖坑填数+分治法
- 原理:
1.先从数列中取出一个数作为基准数。
2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
3.再对左右区间重复第二步,直到各区间只有一个数。 - 图解:
这个是原理的1和2 - C语言代码:
/*
* 快速排序
*
* 参数说明:
* a -- 待排序的数组
* l -- 数组的左边界(例如,从起始位置开始排序,则l=0)
* r -- 数组的右边界(例如,排序截至到数组末尾,则r=a.length-1)
*/
void quick_sort(int a[], int l, int r)
{
if (l < r)
{
int i,j,x;
i = l;
j = r;
x = a[i];
while (i < j)
{
while(i < j && a[j] > x)
j--; // 从右向左找第一个小于x的数
if(i < j)
a[i++] = a[j];
while(i < j && a[i] < x)
i++; // 从左向右找第一个大于x的数
if(i < j)
a[j--] = a[i];
}
a[i] = x;
quick_sort(a, l, i-1); /* 递归调用 */
quick_sort(a, i+1, r); /* 递归调用 */
}
}
选择排序类
简单选择排序
参考文章:http://c.biancheng.net/view/3446.html
- 原理:对于具有 n 个记录的无序表遍历 n-1 次,第 i 次从无序表中第 i 个记录开始,找出后序关键字中最小的记录,然后放置在第 i 的位置上。
- 图解:例如对无序表{56,12,80,91,20}采用简单选择排序算法进行排序,具体过程为:
- 从下标为1的位置开始,遍历后面关键字,如果有比下标为0小的,同下标为0的关键字交换。
- 从下标为2的位置开始,遍历后面关键字,如果有比下标为1小的,同下标为1的关键字交换。
- 从下标为3的位置开始,遍历后面关键字,如果有比下标为2小的,同下标为2的关键字交换。
- 从下标为4的位置开始,遍历后面关键字,如果有比下标为3小的,同下标为3的关键字交换。
- 从下标为1的位置开始,遍历后面关键字,如果有比下标为0小的,同下标为0的关键字交换。
- C语言代码:
void SelectionSort(int arr[], int n) //升序
{
int i, j, temp, min;
for(i = 0; i < n; i++)
{
min = i;
for(j = i+1; j < n; j++)
if(arr[min] > arr[j]) min = j;
if(min != i) //如果 j 和 i 不相等,说明最小值不在下标为 i 的位置,需要交换
{
temp = arr[min];
arr[min] = arr[i];
arr[i] = temp;
}
}
}
堆排序
参考文章:https://www.cnblogs.com/linhaostudy/p/11761994.html
-
基本概念:
堆排序是一种树形选择排序,在排序过程中可以把元素看成是一颗完全二叉树,每个节点都大(小)于它的两个子节点,当每个节点都大于等于它的两个子节点时,就称为大顶堆,也叫堆有序; 当每个节点都小于等于它的两个子节点时,就称为小顶堆。
即:- k(i) ≤ k(2i) 且 k(i) ≤ k(2i+1)(在 n 个记录的范围内,第 i 个关键字的值小于第 2i 个关键字,同时也小于第 2i+1 个关键字)
- k(i) ≥ k(2i) 且 k(i) ≥ k(2i+1)(在 n 个记录的范围内,第 i 个关键字的值大于第 2i 个关键字,同时也大于第 2i+1 个关键字)
-
原理:
1.将长度为n的待排序的数组进行堆有序化构造成一个大顶堆
2.将根节点与尾节点交换并输出此时的尾节点
3.将剩余的n -1个节点重新进行堆有序化
4.重复步骤2,步骤3直至构造成一个有序序列 -
图解:
eg:{5, 2, 6, 0, 3, 9, 1, 7, 4, 8}
1.最大堆的构建
2.将最大值和最后的一个元素交换
3.将剩余的结点再次进行堆构造,回到第一步
4.将最大值和最后的一个元素交换
5.回到第一步,最终结果:
-
个人理解
构造最大堆的过程:
父结点下沉、孩子结点上浮:指的是父结点与孩子结点中最大的比较,孩子结点中最大的上浮,如下所示:
接下来以根结点为2所在的子树进行分析:
最后就是到了这棵树的根结点下沉:
排序的过程:
上面大顶堆已经构造完成,我们现在需要排序,每次将最大的元素和最后一个元素进行交换,然后将剩余元素构成的二叉树重新调整,即对根结点进行下沉、孩子结点进行上浮操作。
-
算法分析
时间效率:O(nlog2n) 对初始序列不敏感,最好、最坏和平均时间复杂度均为O(nlog2n)
空间效率:O(1):只需要一个辅助存储空间
稳 定 性:不稳定,适用于n 较大的情况,初始建堆所需的比较次数较多,不适合序列长度较小的情况。 -
C语言代码:
// 大顶堆的构造,对父结点进行下沉、孩子结点上浮
// 注意树的根结点为k[1],则左、右孩子结点分别为k[2]和k[3]
void HeapAdjust(int k[], int p, int n)
{
int i,temp;
temp = k[p]; // 备份父节点的值
for (i = 2 * p; i <= n; i*=2) // 逐渐去找左右孩子结点
{
// 找到两个孩子结点中最大的
if (i < n && k[i] < k[i + 1])
i++;
// 父节点和孩子最大的进行判断
if (temp >= k[i]) // 如果父结点大于孩子结点,则已经为最大堆,终止父结点下沉、孩子结点上浮操作
break;
k[p] = k[i]; // 孩子结点上浮
p = i; // 更新该子树的根结点作为父节点
}
// 当我们在for循环中孩子结点上浮,最后一次上浮的孩子结点的值更新为最开始时父节点的值
// 若没有找到,相当于其值不变
k[p] = temp;
}
void swap(int k[], int i, int j)
{
int temp = k[i];
k[i] = k[j];
k[j] = temp;
}
// 大顶堆排序,注意树的根结点为k[1]
void HeapSort(int k[], int n)
{
int i;
// 首先将无序数列转换为大顶堆
for (i = n / 2; i > 0;i--) // 由于是完全二叉树,所以我们从一半向前构造,传入父节点
HeapAdjust(k, i, n);
// 上面大顶堆已经构造完成,我们现在需要排序,每次将最大的元素和最后一个元素进行交换
// 然后将剩余元素重新调整,即对根结点进行下沉、孩子结点进行上浮操作
for (i = n; i > 1; i--)
{
swap(k, 1, i);
HeapAdjust(k, 1, i - 1);
}
}
插入排序类
直接插入排序
参考文章:http://c.biancheng.net/view/3439.html
- 原理:在添加新的记录时,使用顺序查找的方式找到其要插入的位置,然后将新记录插入。
- 图解:例如采用直接插入排序算法将无序表{3,1,7,5,2,4,9,6}进行升序排序的过程为:
- C语言代码:
void InsertSort(int arr[], int n)
{
int i, j, temp;
for(i = 1; i < n; i++)
if(arr[i] < arr[i-1]) //若第 i 个元素大于 i-1 元素则直接插入;反之,需要找到适当的插入位置后在插入。
{
temp = arr[i];
//采用顺序查找方式找到插入的位置,在查找的同时,将数组中的元素进行后移操作,给插入元素腾出空间
for(j = i-1; j > -1 && temp < arr[j]; j--)
arr[j+1] = arr[j];
arr[j+1] = temp; //插入到正确位置
}
}
void InsertSort(int a[], int n)
{
int i, j;
for(i = 2; i <= n; i++)
if(a[i] < a[i-1]) // 将a[i]插入有序子表
{
a[0] = a[i]; // 复制为哨兵
for(j = i-1; a[0] < a[j]; j--)
a[j+1] = a[j]; // 记录后移
a[j+1] = a[0]; // 插入到正确位置
}
}
折半排序
void BInsertSort(int a[], int size)
{
int i, j, low, high, mid;
int temp = 0;
for (i=1; i<size; i++)
{
low = 0;
high = i-1;
temp = a[i];
while (low <= high) // 采用折半查找法判断插入位置,最终变量 low 表示插入位置
{
mid = (low + high) / 2;
if(temp < a[mid]) high = mid-1;
else low = mid + 1;
}
for (j = i-1; j >= low; j--) // 有序表中插入位置后的元素统一后移
a[j+1] = a[j];
a[low] = temp; //插入元素
}
}
void BInsertSort(int a[], int n)
{
int i, j, low, high, mid;
for(i=2; i <= n; i++)
{
a[0] = a[i]; // 复制为哨兵
low = 1; high = i-1;
while (low <= high)
{
mid = (low + high) / 2; // 折半
if (a[0] < a[mid]) high = mid-1; // 插入点在低半区
else low = mid + 1 ; // 插入点在高半区
}
for(j = i-1; j >= low; j--)
a[j+1] = a[j]; // 记录后移
a[low] = a[0]; // 插入到正确位置
}
}
希尔排序
参考文章:https://blog.csdn.net/qq_39207948/article/details/80006224
- 原理:先取一个小于n的整数d,作为第一个增量,把所有距离为d的数据分组,再对每一组进行直接插入排序,最后合并。接着,增量减半,分组排序合并,重复下去直到增量为1.
- 图解:
最后在进行一次直接插入排序就OK了。 - C语言代码:
void ShellSort(int arr[], int n)
{
int i, j, temp, gap; // gap是分组的步长
for(gap = n/2; gap > 0; gap = gap/2)
{
for(i = gap; i < n; i++) // 每组交替插入排序
{
if(arr[i] < arr[i - gap])
{
temp = arr[i];
for(j = i - gap; j > -1 && temp < arr[j]; j = j - gap)
arr[j + gap] = arr[j]; // 记录后移
arr[j + gap] = temp; // 插入到正确位置
}
}
}
}
void ShellInsert(int a[], int dk, int length)
{
int i, j;
for(i = dk+1; i <= length; i++) // 每组交替插入排序
if(a[i] < a[i-dk])
{
a[0] = a[i]; // 复制为哨兵
for(j = i-dk; j > 0 && (a[0] < a[j]); j = j-dk)
a[j+dk] = a[j]; // 记录后移
a[j+dk] = a[0]; // 插入到正确位置
}
}
void ShellSort(int a[], int n)
{
int dk;
for(dk = n/2; dk > 0; dk /= 2) // 增量从n/2开始,每次除2
ShellInsert(a, dk, n);
}
归并排序类
归并排序
参考文章:https://zhuanlan.zhihu.com/p/124356219
- 原理:
分解(Divide):将n个元素分成个含n/2个元素的子序列。
解决(Conquer):用合并排序法对两个子序列递归的排序。
合并(Combine):合并两个已排序的子序列已得到排序结果。 - 图解:
- C语言代码:
迭代法:
① 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
② 设定两个指针,最初位置分别为两个已经排序序列的起始位置
③ 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
④ 重复步骤③直到某一指针到达序列尾
⑤ 将另一序列剩下的所有元素直接复制到合并序列尾
int min(int x, int y) {
return x < y ? x : y;
}
void merge_sort(int arr[], int len) {
int* a = arr;
int* b = (int*) malloc(len * sizeof(int));
int seg, start;
for (seg = 1; seg < len; seg += seg) {
for (start = 0; start < len; start += seg + seg) {
int low = start, mid = min(start + seg, len), high = min(start + seg + seg, len);
int k = low;
int start1 = low, end1 = mid;
int start2 = mid, end2 = high;
while (start1 < end1 && start2 < end2)
b[k++] = a[start1] < a[start2] ? a[start1++] : a[start2++];
while (start1 < end1)
b[k++] = a[start1++];
while (start2 < end2)
b[k++] = a[start2++];
}
int* temp = a;
a = b;
b = temp;
}
if (a != arr) {
int i;
for (i = 0; i < len; i++)
b[i] = a[i];
b = a;
}
free(b);
}
递归法:
① 将序列每相邻两个数字进行归并操作,形成floor(n/2)个序列,排序后每个序列包含两个元素
② 将上述序列再次归并,形成floor(n/4)个序列,每个序列包含四个元素
③ 重复步骤②,直到所有元素排序完毕
void merge_sort_recursive(int arr[], int reg[], int start, int end)
{
if (start >= end) return;
int len = end - start, mid = (len >> 1) + start;
int start1 = start, end1 = mid;
int start2 = mid + 1, end2 = end;
merge_sort_recursive(arr, reg, start1, end1);
merge_sort_recursive(arr, reg, start2, end2);
int k = start;
while (start1 <= end1 && start2 <= end2)
reg[k++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];
while (start1 <= end1)
reg[k++] = arr[start1++];
while (start2 <= end2)
reg[k++] = arr[start2++];
for (k = start; k <= end; k++)
arr[k] = reg[k];
}
void MergeSort(int arr[], const int len) {
int *tmp = (int *)malloc(len*sizeof(int));
merge_sort_recursive(arr, tmp, 0, len - 1);
free(tmp);
}