简述
排序算法的稳定性:如果 Ai=Aj ,排序前后 Ai 和 Aj 的相对位置不变,则称这种排序算法是稳定的,反之,则是不稳定的。
排序算法稳定性的好处:排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。例如基数排序,先按低位排序,逐次按高位排序,低位排序后元素的顺序在高位也相同时是不会改变的。
插入排序
- 直接插入
- 二分插入
(1)直接插入:类似于扑克牌插入,对于未排序数据(右手抓到的牌),在已排序序列(左手已经排好序的手牌)中从后向前扫描,找到相应位置并插入。
空间复杂度:在实现上,需用到 O(1) 的额外空间,因为在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
具体算法描述如下:
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果该元素(已排序)大于新元素,将该元素移到下一位置
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置后
- 重复步骤2~5
// 分类 ---------- 内部比较排序
// 数据结构 ------- 数组
// 最差时间复杂度 -- 输入序列是降序排列的,此时时间复杂度O(n^2)
// 最优时间复杂度 -- 输入序列是升序排列的,此时时间复杂度O(n)
// 平均时间复杂度 -- O(n^2)
// 所需辅助空间 --- O(1)
// 稳定性 -------- 稳定
int *insertSort(int *array, int num)
{
int i, j, target;
for(i = 1; i < num; i++)
{
target = array[i];// 右手抓到一张扑克牌
j = i;
while(target < array[j-1] && j >= 1)// 将抓到的牌与手牌从右向左进行比较
{
array[j] = array[j-1];// 如果该手牌比抓到的牌大,就将其右移
j--;
}
//直到该手牌比抓到的牌小或相等,将抓到的牌插入到该手牌右边(相等元素的相对次序未变,所以插入排序是稳定的)
array[j] = target;
}
return array;
}
注意:插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,比如量级小于千,那么插入排序还是一个不错的选择。 插入排序在工业级库中也有着广泛的应用,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。
(2)二分插入:如果比较操作的代价比交换操作大的话,可以采用二分查找法来减少比较操作的数目,称为二分插入排序。
int *insertSort(int *array, int num)
{
int i, j, target;
int left, right, middle;
for(i = 1; i < num; i++)
{
target = array[i];// 右手抓到一张扑克牌
left = 0;
right = i - 1;
while(left <= right)//二分查找过程
{
middle = (left + right) / 2;
if(array[middle] > target)
right = middle - 1;
else
left = middle + 1;
}
for(j = i - 1; j >= left; j--)// 右移
{
array[j] = array[j-1];
}
array[left] = target;
}
return array;
}
冒泡排序
基本思想:依次比较相邻的两个元数,将小的数放在前面,大的数放在后面。
具体算法描述如下:
- 比较相邻的元素,如果前一个比后一个大,就把它们两个调换位置。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(n^2)
// 最优时间复杂度 ---- 原始数据有序,在内部循环中使用一个标识来表示有无交换操作,可以把最优时间复杂度降低到O(n)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 稳定
int *bubble(int *array, int num)
{
int i, j, temp;
for(i = 0; i < num; i++)
{
for(j = 0; j < num-i-1; j++)
{
if(array[j+1] < array[j])
{
temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
}
return array
}
冒泡排序的改进:鸡尾酒排序:也叫定向冒泡排序。冒泡排序则仅从低到高去比较序列里的每个元素,而鸡尾酒排序是从低到高然后从高到低去比较序列中的每个元素。可以得到比冒泡排序稍微好一点的效能。
#include <stdio.h>
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(n^2)
// 最优时间复杂度 ---- 如果序列在一开始已经大部分排序过的话,会接近O(n)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 稳定
void exchange(int A[], int i, int j) // 交换A[i]和A[j]
{
int temp = A[i];
A[i] = A[j];
A[j] = temp;
}
int main()
{
int A[] = { 6, 5, 3, 1, 8, 7, 2, 4 }; // 从小到大定向冒泡排序
int n = sizeof(A) / sizeof(int);
int left = 0; // 初始化边界
int right = n - 1;
while (left < right)
{
for (int i = left; i < right; i++) // 前半轮,将最大元素放到后面
if (A[i] > A[i + 1])
{
exchange(A, i, i + 1);
}
right--;
for (int i = right; i > left; i--) // 后半轮,将最小元素放到前面
if (A[i - 1] > A[i])
{
exchange(A, i - 1, i);
}
left++;
}
return 0;
}
选择排序
基本思想:遍历列表,并且将最小的元素与第一个元素交换;再次遍历剩余的元素并将次小的元素与第二个元素交换,依次类推。
选择排序是不稳定的排序算法,不稳定发生在最小元素与A[i]交换的时刻。
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(n^2)
// 最优时间复杂度 ---- O(n^2)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 不稳定
int *selectSort(int *array, int n)
{
int smallIndex, i, j, temp;
for (i= 0; i< n - 1; i++)
{
// 假定最小值初始时是arr[i]=1st
smallIndex = i;
// 遍历子列表,从arr[i+1]到arr[n-1],因为从0到i已经是有序的了
for (j = i+ 1; j < n; j++)
{
//如果发现小元素,把当前元素的索引赋值给最小值
if (arr[j] < arr[smallIndex])
{
smallIndex = j;
}
}
//交换
temp = arr[i];
arr[i] = arr[smallIndex];
arr[smallIndex] = temp;
}
}
注意:插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,比如量级小于千,那么插入排序还是一个不错的选择。 插入排序在工业级库中也有着广泛的应用,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。
(2)二分插入:如果比较操作的代价比交换操作大的话,可以采用二分查找法来减少比较操作的数目,称为二分插入排序。
int *insertSort(int *array, int num)
{
int i, j, target;
int left, right, middle;
for(i = 1; i < num; i++)
{
target = array[i];// 右手抓到一张扑克牌
left = 0;
right = i - 1;
while(left <= right)//二分查找过程
{
middle = (left + right) / 2;
if(array[middle] > target)
right = middle - 1;
else
left = middle + 1;
}
for(j = i - 1; j >= left; j--)// 右移
{
array[j] = array[j-1];
}
array[left] = target;
}
return array;
}
希尔排序
也叫递减增量排序,是插入排序的一种更高效的改进版本。希尔排序是不稳定的排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位
基本思想:先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率上比前两种方法有较大提高。
具体算法描述如下:
以n=10的一个数组49, 38, 65, 97, 26, 13, 27, 49, 55, 4为例
第一次 gap = 10 / 2 = 5,即分成了5组(49, 13) (38, 27) (65, 49) (97, 55) (26, 4),这样每组排序后就变成了(13, 49) (27, 38) (49, 65) (55, 97) (4, 26)
第二次 gap = 5 / 2 = 2,即分成了2组(13,49,4,38,97)(27,55,49,65,26),排序后变成了(4,13,38,49,97)(26,27,49,55,65)
第三次 gap = 2 / 2 = 1,即分成了1组,直接使用插入排序,最后得到(4 13 26 27 38 49 49 55 65 97)
第四次 gap = 1 / 2 = 0 排序完成
void shellsort1(int a[], int n)
{
int i, j, gap;
for (gap = n / 2; gap > 0; gap /= 2) //步长
for (i = 0; i < gap; i++) //直接插入排序
{
for (j = i + gap; j < n; j += gap)
if (a[j] < a[j - gap])
{
int temp = a[j];
int k = j - gap;
while (k >= 0 && a[k] > temp)
{
a[k + gap] = a[k];
k -= gap;
}
a[k + gap] = temp;
}
}
}
对上面的代码进行简化
void shellsort3(int a[], int n)
{
int i, j, gap;
for (gap = n / 2; gap > 0; gap /= 2)
for (i = gap; i < n; i++) //从数组第gap个元素开始
for (j = i - gap; j >= 0 && a[j] > a[j + gap]; j -= gap) //每个元素与自己组内的数据进行直接插入排序
Swap(a[j], a[j + gap]);
}
快速排序
基本思想:挖坑填数+分治法
1. 先从数列中取出一个数作为基准数。
2. 分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
3. 再对左右区间重复第二步,直到各区间只有一个数。
具体算法描述如下:
例如无序数组[6 2 4 1 5 9]
a. 先把第一项[6]取出来,用[6]依次与其余项进行比较,如果比[6]小就放[6]前边,否则就放[6]后边。排序前 6 2 4 1 5 9,排序后 5 2 4 1 6 9
b. 对前半拉[5 2 4 1]继续进行快速排序,重复步骤a后变成下边这样:
排序前 5 2 4 1,排序后 1 2 4 5
前半拉排序完成,总的排序也完成。
// 分类 ------------ 内部比较排序
// 数据结构 --------- 数组
// 最差时间复杂度 ---- 每次选取的基准都是最大的元素(或者每次都是最小),导致每次只划分出了一个子序列,需要进行n-1次划分才能结束递归,时间复杂度为O(n^2)
// 最优时间复杂度 ---- 每次选取的基准都能使划分均匀,只需要logn次划分就能结束递归,时间复杂度为O(nlogn)
// 平均时间复杂度 ---- O(nlogn)
// 所需辅助空间 ------ O(logn)~O(n),主要是递归造成的栈空间的使用(用来保存left和right等局部变量),取决于递归树的深度,一般为O(logn),最差为O(n)(基本有序的情况)
// 稳定性 ---------- 不稳定
int patition(int *array, int low, int high)
{
int temp = array[low];
while(low<high)
{
while(low < high && array[high] > temp) high--;
array[low] = array[high];
while(low < high && array[low] <= temp) low++;
array[high] = array[low];
for(int i = 0; i < high+1; i++)
{
printf("%d\t",array[i]);
}
printf("\n");
}
array[low] = temp;
return low;
}
int *quick(int *array, int low, int high)
{
int mid = 0;
if(low < high)
{
mid = patition(array,low,high);
quick(array,low,mid-1);
quick(array,mid+1,high);
}
return array;
}
对上面的代码进行整合:
void quick_sort(int s[], int l, int r)
{
if (l < r)
{
//Swap(s[l], s[(l + r) / 2]); //将中间的这个数和第一个数交换 参见注1
int i = l, j = r, x = s[l];
while (i < j)
{
while(i < j && s[j] >= x) // 从右向左找第一个小于x的数
j--;
if(i < j)
s[i++] = s[j];
while(i < j && s[i] < x) // 从左向右找第一个大于等于x的数
i++;
if(i < j)
s[j--] = s[i];
}
s[i] = x;
quick_sort(s, l, i - 1); // 递归调用
quick_sort(s, i + 1, r);
}
}
归并排序
基本思想:将两个或两个以上的有序数据序列合并成一个新的有序数据序列。假设数组A有N个元素,可以将数组A看成是由N个有序的子序列组成,每个子序列的长度为1,然后再两两合并,得到了一个 N/2 个长度为2或1的有序子序列,再两两合并,如此重复,直到得到一个长度为N的有序数据序列为止。
例如无序数组[6 2 4 1 5 9]
第一步 [6 2 4 1 5 9]原始状态
第二步 [2 6] [1 4] [5 9]两两合并排序
第三步 [1 2 4 6] [5 9]继续两组两组合并
第四步 [1 2 4 5 6 9]合并完毕,排序完毕
输出结果[1 2 4 5 6 9]
合并细节
第二步:[2 6] [1 4] [5 9],两两合并,其实仅合并[2 6] [1 4],所以[5 9]不管它
原始状态
第一个数组[2 6]
第二个数组[1 4]
第三个数组[…]
第1步,顺序从第一,第二个数组里取出一个数字:2和1 比较大小后将小的放入第三个数组
第一个数组[2 6]
第二个数组[4]
第三个数组[1]
第2步,继续刚才的步骤,顺序从第一,第二个数组里取数据,2和4,同样的比较大小后将小的放入第三个数组
第一个数组[6]
第二个数组[4]
第三个数组[1 2]
第3步,再重复前边的步骤变成,将较小的4放入第三个数组后变成如下状态
第一个数组[6]
第二个数组[…]
第三个数组[1 2 4]
第4步,最后将6放入,排序完毕
第一个数组[…]
第二个数组[…]
第三个数组[1 2 4 6]
具体算法描述如下:
- 将一个列表分割成2个子列表
- 第1个列表调用索引[low,mid]来定义, 第2个列表调用索引[mid+1,high]来定义
- 将两个有序的子文件R[low..mid)和R[mid+1..high]归并成一个有序的子文件R[low..high]
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(nlogn)
// 最优时间复杂度 ---- O(nlogn)
// 平均时间复杂度 ---- O(nlogn)
// 所需辅助空间 ------ O(n)
// 稳定性 ------------ 稳定
//将有二个有序数列a[first...mid]和a[mid...last]合并。
void mergearray(int a[], int first, int mid, int last, int temp[])
{
int i = first, j = mid + 1;
int m = mid, n = last;
int k = 0;
while (i <= m && j <= n)
{
if (a[i] <= a[j])
temp[k++] = a[i++];
else
temp[k++] = a[j++];
}
while (i <= m)
temp[k++] = a[i++];
while (j <= n)
temp[k++] = a[j++];
for (i = 0; i < k; i++)
a[first + i] = temp[i];
}
void mergesort(int a[], int first, int last, int temp[])
{
if (first < last)
{
int mid = (first + last) / 2;
mergesort(a, first, mid, temp); //左边有序
mergesort(a, mid + 1, last, temp); //右边有序
mergearray(a, first, mid, last, temp); //再将二个有序数列合并
}
}
bool MergeSort(int a[], int n)
{
int *p = new int[n];
if (p == NULL)
return false;
mergesort(a, 0, n - 1, p);
delete[] p;
return true;
}
堆排序
二叉堆的定义:二叉堆是完全二叉树或者是近似完全二叉树。
当父结点的键值总是大于或等于任何一个子节点的键值时为最大堆。当父结点的键值总是小于或等于任何一个子节点的键值时为最小堆。
堆排序的过程:
1. 创建一个堆
2. 把堆顶元素(最大值)和堆尾元素互换
3. 把堆的尺寸缩小1,并调用从新的堆顶元素开始进行堆调整
4. 重复步骤2,直到堆的尺寸为1
堆排序是不稳定的排序算法,不稳定发生在堆顶元素与A[i]交换的时刻。
#include <iostream>
#include <vector>
using namespace std;
int heapsize;
// 交换A[i]和A[j]
void exchange(vector<int> &A, int i, int j)
{
int temp = A[i];
A[i] = A[j];
A[j] = temp;
}
// 堆调整函数(这里使用的是最大堆),调整第i个结点
void heapify(vector<int> &A, int i)
{
int leftchild = 2 * i + 1; // 左孩子索引
int rightchild = 2 * i + 2; // 右孩子索引
int largest; // 选出当前结点与左右孩子之中的最大值
if (leftchild < heapsize && A[leftchild] > A[i])
largest = leftchild;
else
largest = i;
if (rightchild < heapsize && A[rightchild] > A[largest])
largest = rightchild;
if (largest != i) //最大值结点不是当前结点i
{
exchange(A, i, largest); // 把当前结点和它的最大(直接)子节点进行交换
heapify(A, largest); // 递归调用,继续从当前结点向下进行堆调整
}
}
void buildMaxHeap(vector<int> &num, int n){
heapsize = n; //堆大小
for(int i = heapsize/2-1; i >= 0; i--) // 对每一个非叶结点
heapify(num, i); // 不断的堆调整
}
//堆排序
void heapsort(vector<int> &A, int n)
{
buildMaxHeap(A, n);
for(int i=n-1; i >= 1; i--)
{
exchange(A, 0, i); // 将堆顶元素(当前最大值)与堆的最后一个元素互换(该操作很有可能把后面元素的稳定性打乱,所以堆排序是不稳定的排序算法)
heapsize--; // 从堆中去掉最后一个元素
heapify(A, 0); // 从新的堆顶元素开始进行堆调整
}
}
int main(){
int a[] = {9,12,17,30,50,20,60,65,4,49};
vector<int> num (a,a+10);
int len = num.size();
heapsort(num, len);
for(int i=0; i<len; i++){
cout<<num[i]<<'\t';
}
return 0;
}