一、八大排序算法的总体比较
持续更新~~
1、怎么判断稳定性?
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
2、来~手写一个快排/堆排。。。
不稳定:快选东(堆)希~
二、各自的特点以及实现
1、快排
(1)算法思想:
通过一趟排序讲排序数组分成两部分,一部分比中枢值小,一部分比中枢值大,再分别对这两个部分继续进行排序,最终达到整个数组有序的目的。
基准的选择:
a.三数取中
具体思想:对待排序序列中low、mid、high三个位置上数据进行排序,取他们中间的那个数据作为枢轴,并用0下标元素存储枢轴;
b.随机选取基准
引入原因:在待排序列是部分有序时,固定选取枢轴使快排效率低下;
具体思想:取在待排序列中任意一个元素作为基准;
(2)效率分析:
此排序算法的效率在序列越乱的时候,效率越高。在数据有序时,会退化成冒泡排序;
(3)优化方案:
a.当待排序序列的长度分割到一定大小后,使用插入排序;
原因:对于很小和部分有序的数组,快排不如插排好。当待排序序列的长度分割到一定大小后,继续分割的效率比插入排序要差,此时可以使用插排而不是快排;
b.在一次分割结束后,可以把与key相等的元素聚集在一起,继续下次分割时,不必再对于key相等元素分割;
(4)C++代码:
//
int partition1(vector<int> &arr, int first,int last) {
int pivotkey = arr[first];
while (first < last ){
while (first < last && arr[last] >= pivotkey)
last--;
swap(arr[first], arr[last]);
while (first < last && arr[first] <= pivotkey)
first++;
swap(arr[first], arr[last]);
}
return first;
}
void quicksort1(vector<int> &arr, int first, int last) {
int pivot;
if (first < last) {
pivot = partition1(arr, first, last);
quicksort1(arr, first, pivot - 1);
quicksort1(arr, pivot + 1, last);
}
}
void QuickSort1(vector<int> &arr) {
quicksort1(arr, 0, arr.size()-1);
}
2、冒泡排序
(1)算法思想:
在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现他们的排序与排序要求相反时,就将他们互换。
(2)效率分析:
优点:稳定,快
缺点:比较次数不一定,比较次数越少,插入点后的数据移动越多,特别是数据量庞大的时候
时间复杂度:平均情况:O[n^2] 最好情况O[n] 最坏情况 O[n^2]
空间复杂度:O[1]
稳定性:稳定
(3)优化方案:
不发生交换的时候已经排好序了,不需要再循环(详细见如下代码)
(4)C++代码:
//冒泡排序 平均时间复杂度O(n^2)
void BubbleSort1(vector<int>&iter) {
int i, j;
for (i = 0; i < iter.size(); ++i) {
for (j = iter.size() - 2; j >=0; --j){
if (iter[j] > iter[j + 1])
swap(iter[j], iter[j + 1]);
}
}
}
//优化版的冒泡排序,(不发生交换的时候已经排好序了,不需要再循环)
void BubbleSort2(vector<int>&iter) {
bool flag = true;
int i, j;
for (i = 0; i < iter.size() && flag; i++) {
flag = false;
for (j = iter.size() - 2; j >= 0; j--) {
if (iter[j] > iter[j + 1]) {
swap(iter[j], iter[j + 1]);
flag = true;
}
}
}
}
3、直接插入排序
(1)算法思想:
每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子序列中的适当位置,直到全部记录插入完成为止。
(2)效率分析:
时间复杂度:平均情况:O[n^2] 最好情况O[n] 最坏情况 O[n^2]
空间复杂度:O[1]
稳定性:稳定
(3)优化方案:
将搜索和数据后移两个步骤合并(代码只给出了优化后的结果)
(4)C++代码:
//插入排序(外层循环是需要被插入的数,内循环比较在有序队列中的位置,考虑和前一个的比较)
//最好 O(n),最差O(n^2)
void InsertSort(vector<int> &arr) {
for (int i = 1; i < arr.size(); i++) {
for (int j = i - 1; j >=0 && arr[j]>arr[j+1]; j--) {
swap(arr[j],arr[j+1]);
}
}
}
4、堆排序
(1)算法思想:
利用堆进行(大顶堆)排序的方法,将待排序的序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根结点。将根结点移走(将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个大顶堆,这样就会得到n个元素中的次大值,如此反复执行,便能得到一个有序序列。
需要解决两个重要的问题:如何将一个无序序列构建成一个大顶堆?在输出堆顶元素之后,如何调整剩余元素成为一个新的大顶堆?
(2)效率分析:
时间复杂度:平均情况:O[nlogn] 最好情况O[nlogn] 最坏情况 O[nlogn]
空间复杂度:O[1]
稳定性:不稳定
(3)优化方案:
(4)C++代码:
void Heapajust(vector<int> &arr, int s, int m){
int j, temp;
temp = arr[s - 1]; //保存当前位置的值
for (j = 2 * s; j <= m; j *= 2){
if (j < m&&arr[j - 1] < arr[j])
++j; //j为较大值的编号
if (temp>=arr[j-1])
break;
arr[s - 1] = arr[j-1];
s = j;
}
arr[s - 1] = temp;
}
void HeapAdjustSort(vector<int> &arr){
int i;//i为大顶堆编号,从1到n(arr.size()),而不是数组下标
for (i = arr.size() / 2; i > 0; --i)
Heapajust(arr, i, arr.size()); //构建大顶堆
for (i = arr.size(); i > 1; --i){
swap(arr[0], arr[i - 1]); //将堆顶元素与最后一个元素交换
Heapajust(arr,1,i-1); //调整剩下元素构成的堆
}
}
5、希尔排序
(1)算法思想:
直接插入排序在某些情况下,效率是比较高的,比如,记录本身是基本有序的,只需要少量的插入操作就可以完成整个记录集的排序工作;记录数比较少的时候。这样的条件比较苛刻,需要创造条件,由此在直接插入排序的基础上出现希尔排序。改进的原理,将原本大量的记录进行分组,分割成如干个子序列,此时每个子序列待排序的记录个数就减少了,然后对这些子序列内分别进行直接插入排序,当整个序列基本有序时,在对全体记录进行一次直接插入排序。
分割子序列的方法采取跳跃分割的策略,即将相距某个增量的记录组成一个子序列,这样也可以保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。基本有序指的是小的关键字基本在前面,大的基本在后面,不大不小的基本在中间。
(2)效率分析:
时间复杂度:平均情况:O[nlogn]~O[n^2] 最好情况O[n^1.5] 最坏情况 O[n^2]
空间复杂度:O[1]
稳定性:不稳定
(3)优化方案:
(4)C++代码:
//希尔排序 复杂度为O(n^1.5),跳跃式移动,不稳定
void ShellSort(vector<int> &arr){
int i, j, temp;
int increment = arr.size();
do
{
increment = increment / 3 + 1;
for (i = increment; i < arr.size(); ++i){
if (arr[i] < arr[i - increment]){
temp = arr[i]; //temp为设置的哨兵,保存当前值
for (j = i - increment; j >= 0 && arr[j]>temp; j -= increment)
arr[j + increment] = arr[j]; //右移
arr[j + increment] = temp; //插入到正确位置
}
}
} while (increment>1);
}
6、归并排序
(1)算法思想:
归并的含义是将两个或两个以上的有序表组合成一个新的有序表。归并排序就是利用归并的思想实现的排序方法。具体的原理是,假设初始序列含有n个记录,则可以看成n个有序的子序列,每个子序列的长度为1,然后两两归并,得到 个长度为2或1的有序子序列;再两两归并,……,如此重复,直至得到一个长度为n的有序序列为止。其实现可分为递归和非递归实现。
递归实现,主要有三部分:① 分解:将当前区间一分为二,即求分裂点 mid = (low + high)/2; ② 求解:递归地对两个子区间a[low…mid] 和 a[mid+1…high]进行归并排序。递归的终结条件是子区间长度为1。③ 合并:将已排序的两个子区间a[low…mid]和 a[mid+1…high]归并为一个有序的区间a[low…high]。
非递归实现:可以看作是递归实现的下半部分。弥补了递归造成的时间和空间上的消耗。
(2)效率分析:
时间复杂度:平均情况:O[nlogn] 最好情况O[nlogn] 最坏情况 O[nlogn]
空间复杂度:O[n]
稳定性:稳定
(3)优化方案:
非递归版本的归并 使用迭代,性能提高 (已在代码中给出)
(4)C++代码:
//两路归并,将两个有序区间合成一个有序区间
void Merge(vector<int> &arr, int first, int mid, int last, vector<int> temp){
int i = first, j = mid + 1;
int k = 0;
while (i <= mid&&j <= last){
if (arr[i] < arr[j])
temp[k++] = arr[i++];
else
temp[k++] = arr[j++];
}
while (i <= mid)
temp[k++] = arr[i++];
while (j <= last)
temp[k++] = arr[j++];
for (i = 0; i < k; ++i)
arr[first + i] = temp[i];
}
//方法1 递归版本的归并排序 时间和空间上的性能损耗较大
void mergesort1(vector<int> &arr, int first, int last, vector<int> temp){
if (first < last){
int mid = first + (last - first) / 2;
mergesort1(arr, first, mid, temp);
mergesort1(arr, mid + 1, last, temp);
Merge(arr, first, mid, last, temp);
}
}
//构建临时数组
void MergeSort1(vector<int> &arr){
vector<int> temp(arr.size(), 0); //辅助空间
mergesort1(arr,0,arr.size()-1, temp);
temp.clear();
}
//方法2 非递归版本的归并 使用迭代,性能提高
void MergeSort2(vector<int> &arr){
int n = arr.size();
vector<int> temp(n, 0);
int s = 1; //将相邻长度为s的两个区间有序合并
while (s < n){
for (int i = 0; i < n; i += 2 * s){
if (i + 2 * s - 1 < n)
Merge(arr, i, i + s - 1, i + 2 * s - 1, temp);
else if (i + s - 1 < n)
Merge(arr,i,i+s-1,n-1,temp);
}
s *= 2;
}
temp.clear();
}
7、选择排序
(1)算法思想:
通过n-i次关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1<=i<=n)个记录交换之。
(2)效率分析:
时间复杂度:平均情况:O[n^2] 最好情况O[n^2] 最坏情况 O[n^2]
空间复杂度:O[1]
稳定性:稳定
(3)优化方案:
选择最大值最小值,,可以减少一半的比较次数(优化方案已在代码中给出)
(4)C++代码:
//选择排序(内循环寻找最小或者最大的值,外循环做交换把最小的换到最前面,最大的换到最后面)
void SelectSort1(vector<int> &arr) {
int i, j, min;
for (i = 0; i < arr.size(); i++) {
min = i;
for (j = i + 1; j < arr.size(); j++) {
if (arr[j] < arr[min])
min = j;
}
if (i != min)
swap(arr[i], arr[min]);
}
}
//选择排序改进版,选择最大值最小值,,可以减少一半的比较次数
void SelectSort2(vector<int> &arr) {
int i, j, min, max;
int n = arr.size();
for (i = 0; i < n/2; i++) {
min = i;
max = i;
for (j = i + 1; j < n - i; ++j) {
if (arr[j] < arr[min])
min = j;
else if (arr[j] > arr[max])
max = j;
}
if (i != min)
swap(arr[i], arr[min]);
if (arr[min] > arr[max]) {
max = min;
}
if (n - 1 - i != max)
swap(arr[n-1-i], arr[max]);
}
}
8、基数排序
(1)算法思想:
将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
(2)效率分析:
时间复杂度:平均情况:O[d*(n+r)] 最好情况O[d*(n+r)] 最坏情况 O[d*(n+r)]
空间复杂度:O[n+r]
稳定性:稳定
(3)优化方案:
(4)C++代码:
//基数排序,时间复杂度为O(d(n+r))(d 是最大位数,r 是进制数,一般为10)
//LSD(最低有效位优先)
int maxbit(int *arr, int n){
int d = 1;
int p = 10;
for (int i = 0; i < n; i++){
while (arr[i] >= p){
d++;
p *= 10;
}
}
return d;
}
void RadixSort(int *arr, int n){
// 1.确定最大数字的位数,从而确定排序的趟数d(基数)
int d = maxbit(arr, n);
//2.计数辅助数组,也就是10个桶
int *count = new int[10];
//建立辅助数组
int *temp = new int[n];
int k;
int radix = 1; //用于取元素第i位的值
//3.进行d次分配和收集
for (int i = 1; i <= d; i++){
//置空各个桶的统计数据
for (int j = 0; j < 10; j++)
count[j] = 0;
//统计每个桶中元素的个数
for (int j = 0; j < n; j++){
k = (arr[j] / radix) % 10; //取元素第i位的值
count[k]++;
}
//count[j]表示第j个桶的右边界索引 (计数排序的方法)
for (int j = 1; j < 10; j++)
count[j] += count[j - 1];
for (int j = n - 1; j >= 0; j--){
k = (arr[j] / radix) % 10; //取元素第i位的值
temp[count[k] - 1] = arr[j];
count[k]--;
}
for (int j = 0; j < n; j++)
arr[j] = temp[j];
radix *= 10;
}
delete[]count;
delete[]temp;
}