也不知道与简单排序对应的应该叫什么, 就叫不那么简单的排序好了.
本篇博客主要学习了希尔排序、归并排序and快速排序。
注: 这一篇和上一篇简单排序都算是学习白话算法系列的学习笔记吧
希尔排序
希尔排序是基于插入排序而来, 插入排序的最好时间复杂度是O(n), 当数组基本有序时, 效率是很高的. 而希尔排序, 设定一个增量, 按增量将数组分组.
例如数组{1,2,3,4}, 增量是2, 那么数组就可以分为{1,3}, {2,4}两组, 增量是1那就是1,2,3,4四组.
分组之后在组内进行插入排序, 再缩小增量重新分组再次排序, 直到增量是1(等同于正常的插入排序), 再插入排序一次, 排序完成.
void shellSort(int arr[], int n){
for (int gap = n/2; gap>0; gap/=2) {
for (int i = gap; i<n; i++) {
if (arr[i] < arr[i - gap]) {
int temp = arr[i];
int j;
// 思路与插入排序相同, 用临时变量保存要插入的数, 向数组前面查找插入的位置, 一边查找, 一边将前面较大的数字后移
// 临时变量不小于前面的某数时, 说明找到了正确的位置, 只要放在那个数后面就可以了
for (j = i-gap; j>=0 && temp<arr[j]; j-=gap) {
arr[j+gap] = arr[j];
}
arr[j+gap] = temp;
}
}
}
}
归并排序
归并二字就是递归&合并
归并排序的关键在于合并有序数组, 合并两个有序数组的方式是先比较两数组的第一个元素, 更小的取出放入新数组, 再依次向后比较, 直到某个数组的元素取光, 把另一个数组的元素依次放入新数组既可.
//先来演示合并数组
void mergeArray(int a[], int m, int b[], int n){
int c[m+n];
int i, j, k;
//必须初始化, 否则会有残值
i = j = k = 0;
// 此处不能用for循环, 除非只写第二个表达式, 否则ijk哪个做自增都不合适
// 其中k看似合适, 但for循环最后会执行一次第三个表达式, k会+1
while (i < m && j < n) {
if (a[i] < b[j]) {
c[k++] = a[i++];
}else{
c[k++] = b[j++];
}
}
while (i < m) {
c[k++] = a[i++];
}
while (j < n) {
c[k++] = b[j++];
}
printfArray(c, m+n);
}
下面开始撸正式的归并排序
// 合并有序序列
void mergearray(int arr[], int first, int last, int mid, int temp[]){
int tempIndex = 0;
int firstSequenceIndex = first;
int secondSequeceIndex = mid + 1;
// 因为这里用的是数组角标, 而不是长度, 所以用<= 而不是<
while (firstSequenceIndex <= mid && secondSequeceIndex <= last) {
// 取较小值放入临时数组
if (arr[firstSequenceIndex] < arr[secondSequeceIndex] ) {
temp[tempIndex++] = arr[firstSequenceIndex++];
}else{
temp[tempIndex++] = arr[secondSequeceIndex++];
}
}
// 如果前一个序列还有值, 依次放入临时数组
while (firstSequenceIndex <= mid) {
temp[tempIndex++] = arr[firstSequenceIndex++];
}
// 如果后一个序列还有值, 依次放入临时数组
while (secondSequeceIndex <= last) {
temp[tempIndex++] = arr[secondSequeceIndex++];
}
// 将排好序的部分赋值给原数组
for (int i = 0; i < tempIndex; i++) {
arr[first++] = temp[i];
}
}
// 搞清归并排序, 主要搞清以下两点
// 1. 递归到只有一个数时, 递归函数开始出栈, 一个数肯定是有序序列
// 2. 合并两个有序序列, 可以形成新的有序序列
void mergeSort(int arr[], int first, int last, int temp[]){
if(first < last){
// 将数组分成两部分
int mid = (first + last)/2;
// 前一半排序
mergeSort(arr, first, mid, temp);
// 后一半排序
mergeSort(arr, mid+1, last, temp);
// 合并有序序列
mergearray(arr, first, last, mid, temp);
}
}
快速排序
快速排序是时间复杂度O(logN*N)的排序算法中比较出名的, 面试算法常常会问, 而手写出来是很有难度的事情. 这里非常感谢白话经典算法系列的作者, 讲解通俗易懂.
快速排序的基本思想一句话概括就是<font color='red'>挖坑填数+分治法</font>, 下面详细描述:
- 先取左边第一个数作为基准数
- 与基准数比较, 比基准数大的换到右边, 小的换到左边
- 左右两边分成两个部分, 再进行一次前两步的操作. 重复对左右两边拆分, 进行前两步操作, 直到只剩一个数.
这样说还是太抽象, 举个栗子吧
数组a = {3, 1, 4, 2, 0}
- 取a[0]作为基准数, 使用新变量baseNumber存储
- 从右向左比较, 比基准数小的放在基准数的位置上, 数组变成{<font color='red'>0</font>, 1, 4, 2, <font color='red'>0</font>}, 此时出现一个坑a[4]
- 从左往右比较, 比基准数大的填入上一个坑a[4], 数组变成{0, 1, <font color='red'>4</font>, 2, <font color='red'>4</font>}, 此时的新坑是a[2]
- 再从右向左比较, 比基准数小的填入上一个坑a[2], 数组变成{0, 1, <font color='red'>2</font>, <font color='red'>2</font>, 4}, 此时的坑是a[3]
- 再从左向右比较时, 发现左右相遇了, 将baseNumber赋值给a[3], 数组变成{0, 1, 2, 3, 4}
因为数组元素较少, 这样就排序完成了, 但足够大家了解挖坑填数的思路了.
有一点需要说明, 为什么左右相遇了就可以把baseNumber赋值给那个元素? 因为左右两边相遇时, 所有数字都已经比较了一遍, 已经做到"比基准数大的都在右边, 比基准数小的都在左边".
根据上面的分析, 可以很容易写出挖坑填数的代码:
void changeArray(int arr[], int left, int right){
int i = left;
int j = right;
// 使用变量存储最左边的数做基准数
// 基准数也可不使用最左边的, 中间和最后一个当然都可以
int baseNumber = arr[left];
// 当i=j时意味着数列中所有数都与基准数比较过了, 故结束比较
while (i < j) {
// 从右往左比较, 找到比基准数小的数的下标
while (arr[j] > baseNumber && i < j) {
j--;
}
arr[i] = arr[j];
// 从左往右比较, 找到比基准数大的数的下标
while (arr[i] < baseNumber && i < j) {
i++;
}
arr[j] = arr[i];
}
// 将基准数赋值给a[i](也可以是a[j], 此时i=j)
arr[i] = baseNumber;
}
最后baseNumber赋值,arr[i] = baseNumber
,可能会有人对这句疑惑, 为何可以直接赋值, 不会少一个数吗?
答案是不会, 从上面的代码看出, 即便while (arr[i] < baseNumber && i < j)
这个循环没有走, arr[i]
的值也会赋值给arr[j]
, 这样arr[i]
的值必定有两个, 当然可以直接赋值.
接下来彻底完成递归调用:
void quickSort(int arr[], int left, int right){
// 递归的结束条件, left=right, 也就是只剩一个数的时候
if (left < right) {
int i = left;
int j = right;
int baseNumber = arr[left];
while (i < j) {
while (arr[j] > baseNumber && i < j) {
j--;
}
arr[i] = arr[j];
while (arr[i] < baseNumber && i < j) {
i++;
}
arr[j] = arr[i];
}
arr[i] = baseNumber;
// 递归调用
quickSort(arr, left, i - 1);
quickSort(arr, i + 1, right);
}
}