【数据结构】快速排序与归并排序的实现
1. 快速排序
1.1 算法思想
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
1.2 算法实现
快速排序的实现方式有很多,我们主要介绍三种方法。
1.2.1 Hoare 法
1. 利用两个变量left,right分别指向数组的起始位置与末尾位置。并且以数组第一个元素作为key值。
2. right先从右往左依次遍历找到比key小的数,left从左往右依次遍历找到比key大的数。然后交换left与right下标对应的值。重复步骤2直至right>=left。
3. 之后交换key与left或者right对应的值,并且把该位置记为mid。
4. 最后划分区间[left,mid-1]与[mid+1,right]继续重复1,2步骤。直至不能划分。
思考:为什么最后相遇位置一定小于或等于 key 值?
我们知道right与left
相遇无非两种情况:
情况一:right停住,left移动与right相遇·。因为right一直再找比key小的值,所以right停下位置一定比key小,相遇位置也一定比key小。
情况二:left停住,right移动与left相遇·。此时又分为两种情况:
left从未移动,右侧数据都比key大,相遇位置就是key,交换不变。
left移动过至少一次,也就是至少交换过一次,此时left停留位置的值是上一轮right所对应的值,又因为right一直在找比key小的值,所以相遇位置也一定比key小。
代码实现:
void swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}int PartSort1(int* arr, int begin, int end)
{int left = begin, right = end;int keyi = begin;while (left < right){//left<right防止越界//使用>=而不是>防止数据出现死循环while (left<right && arr[right]>=arr[keyi])//寻找比key小的值{right--;}while (left < right && arr[left] <= arr[keyi])//寻找比key大的值{left++;}swap(&arr[left], &arr[right]);}int mid = left;swap(&arr[keyi], &arr[mid]);return mid;
}void QuickSort(int* arr, int left, int right)
{if (left >= right)//不能划分{return;}int mid = PartSort1(arr, left, right);QuickSort(arr, left, mid - 1);//左区间QuickSort(arr, mid+1, right);//右区间
}
1.2.2 挖坑法
1. 先将起始位置key值设为坑,之后right从右往左找比key值小的值,找到之后放入坑位,此时right就形成新的坑。然后left从左往右找比key大的值, 找到之后放入坑位,此时left就又形成新的坑。
2. 最后left与right相遇,将key放入最后一个坑,并将该位置记为mid,。·
3. 最后划分区间[left,mid-1]与[mid+1,right]继续重复1,2步骤。直至不能划分。
代码实现:
int PartSort2(int* arr, int begin, int end)
{int left = begin, right = end;int hole = begin;//记录坑位int key = arr[left];while (left < right){//left<right防止越界//使用>=而不是>防止数据出现死循环while (left < right && arr[right] >= key)//寻找比key小的值{right--;}arr[hole] = arr[right];hole = right;while (left < right && arr[left] <= key)//寻找比key大的值{left++;}arr[hole] = arr[left];hole = left;}arr[hole] = key;return hole;
}void QuickSort(int* arr, int left, int right)
{if (left >= right)//不能划分{return;}int mid = PartSort2(arr, left, right);QuickSort(arr, left, mid - 1);//左区间QuickSort(arr, mid+1, right);//右区间
}
1.2.3 双指针法
1. 先定义一个prev指向数组首元素,然后定义一个cur指向第二个位置。
2. cur从左往右依次遍历找key小的值,找到之后++prev,然后交换prev与cur指向的值。之后cur++继续遍历。(key为起始位置的值)
3. 当cur遍历完之后,此时交换prev指向的值与key。将此时位置记为mid。
4. 最后划分区间[left,mid-1]与[mid+1,right]继续重复1,2,3步骤。直至不能划分。
代码实现:
int PartSort3(int* arr, int begin, int end)
{int prev = begin;int cur = begin + 1;int keyi = begin;while (cur <= end){if (arr[cur] < arr[keyi])//小于则交换{swap(&arr[++prev], &arr[cur]);}cur++; }swap(&arr[prev], &arr[keyi]);return prev;
}void QuickSort(int* arr, int left, int right)
{if (left >= right)//不能划分{return;}int mid = PartSort3(arr, left, right);QuickSort(arr, left, mid - 1);//左区间QuickSort(arr, mid+1, right);//右区间
}
1.3 算法优化
1.3.1 改变基准元素
当数组有序时,我们再对其进行快速排序,其时间复杂度讲话劣化为O(N2)。
这时候我们为了防止这种现象,可以选择提前改变基准元素 key。
① 三数取中:即取出数组首尾以及中间元素,选取数值位于中间的元素作为准元素 key。
int GetMidNum(int*arr, int left, int right)
{int mid = (left + right) >> 1;if (arr[mid] > arr[left]){if (arr[mid] < arr[right]){ //left mid rightreturn mid;}else if (arr[left] > arr[right]){ //right left midreturn left;}else{ //left right midreturn right;}}
}int PartSort3(int* arr, int begin, int end)
{int prev = begin;int cur = begin + 1;int keyi = begin;int mid=GetMidNum(arr, begin, end);swap(&arr[begin], &arr[mid]);while (cur <= end){if (arr[cur] < arr[keyi])//小于则交换{swap(&arr[++prev], &arr[cur]);}cur++; }swap(&arr[prev], &arr[keyi]);return prev;
}
② 随机数取中:三数取中有时候也并不能保证基准元素的准确性,这时候我们最好使用随机数获取基准值。
int GetRanNum(int*arr, int left, int right)
{srand(time(0));//生成随机种子int mid = rand() % (right - left) + left;//随机数return mid;
}
1.3.2 区间优化
我们进行递归调用时,递归越深递归调用的次数就会越多,为了优化这个问题,我们可以当区间较小时采用其他排序,如插入排序。
void QuickSort(int* arr, int left, int right)
{if (left >= right)//不能划分return;if ((right - left + 1) < 10)//小区间优化{InsertSort(arr+left, right - left + 1);return ;}int mid = PartSort3(arr, left, right);QuickSort(arr, left, mid - 1);//左区间QuickSort(arr, mid+1, right);//右区间
}
2. 归并排序
2.1 算法思想
归并排序(Merge Sort)是建立在归并操作上的一种有效的排序算法, 该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
2.2 算法步骤
1. 创建一个与待排序数组同等大小的tmp数组。
2. 然后将待排序数组分为两个子数组,让两个子数组有序。为了让这两个子数组有序,我们又要将每个子数组分为两个子数组,让其有序。
3. 当子数组没有元素或者只有一个元素时,我们可以认为其有序,然后将两个子数组开始归并。
4. 归并时因为两个子数组有序,我们可以定义两个指针begin1,begin2分别指向两个数组起始位置。然后遍历比较arr[begin1]与arr[begin2],取较小的元素尾插进tmp数组。
5. 最后tmp数组数据拷贝回原数组。
2.3 动画演示
2.4 代码实现
void _MergeSort(int* arr, int begin, int end, int* tmp)
{if (begin >= end)return;int mid = (begin + end) >> 1;_MergeSort(arr, begin, mid, tmp);//归并左区间_MergeSort(arr, mid+1, end, tmp);//归并右区间int i = begin; int begin1 = begin, end1 = mid;int begin2 = mid + 1, end2 = end;while (begin1 <= end1 && begin2 <= end2){if (arr[begin1] < arr[begin2]){tmp[i++] = arr[begin1++];}else{tmp[i++] = arr[begin2++];}}//若是还有区间存在数据while (begin1 <= end1){tmp[i++] = arr[begin1++];}while (begin2 <= end2){tmp[i++] = arr[begin2++];}//最后将归并完后后的数据拷贝回原数组memcpy(arr + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}void MergeSort(int* arr, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc fail:");return;}_MergeSort(arr, 0, n - 1, tmp);free(tmp);tmp = NULL;
}
2.5 算法优化
2.5.1 区间优化
当递归调用层数越多时,最后三层的递归调用会浪费大量时间。为了避免这种情况,这时我们就可以采用小区间使用插入排序的方法。
void _MergeSort(int* arr, int begin, int end, int* tmp)
{if (begin >= end)return;if (end - begin + 1 < 10)//小区间优化{InsertSort(arr + begin, end - begin + 1);return;}int mid = (begin + end) >> 1;_MergeSort(arr, begin, mid, tmp);//归并左区间_MergeSort(arr, mid+1, end, tmp);//归并右区间int i = begin; int begin1 = begin, end1 = mid;int begin2 = mid + 1, end2 = end;while (begin1 <= end1 && begin2 <= end2){if (arr[begin1] < arr[begin2]){ tmp[i++] = arr[begin1++];}else{tmp[i++] = arr[begin2++];}}//若是还有区间存在数据while (begin1 <= end1){tmp[i++] = arr[begin1++];}while (begin2 <= end2){tmp[i++] = arr[begin2++];}//最后将归并完后后的数据拷贝回原数组memcpy(arr + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
2.5.2 判断区间有序
在归并排序合并时,如果两个区间是有序,即 arr[end1] <= arr[begin2] 时就不需要对其进行归并。
void _MergeSort(int* arr, int begin, int end, int* tmp)
{if (begin >= end)return;int mid = (begin + end) >> 1;_MergeSort(arr, begin, mid, tmp);//归并左区间_MergeSort(arr, mid+1, end, tmp);//归并右区间int i = begin; int begin1 = begin, end1 = mid;int begin2 = mid + 1, end2 = end;if (arr[begin2] < arr[end1])//区间有序则不合并{while (begin1 <= end1 && begin2 <= end2){if (arr[begin1] < arr[begin2]){tmp[i++] = arr[begin1++];}else{tmp[i++] = arr[begin2++];}}//若是还有区间存在数据while (begin1 <= end1){tmp[i++] = arr[begin1++];}while (begin2 <= end2){tmp[i++] = arr[begin2++];}//最后将归并完后后的数据拷贝回原数组memcpy(arr + begin, tmp + begin, sizeof(int) * (end - begin + 1));}
}