十大经典排序(C++)
比较排序
选择排序
1、选择排序的时间复杂度分析:
最坏情况下(逆向排序),每一次执行交换都需要对整个数组进行两次遍历,此时数组的时间复杂度为O(n^2)。
交换的次数是O(n)形式的。空间复杂度为O(1)
2、选择排序是非稳定排序,(程序的稳定性与代码设定相关,如果比较条件是大于或者等于,也就是如果两个元素相同则记录第一个出现的值,这样就是稳定排序,如果记录的是后面的值就是非稳定排序,一般稳定性与否都具有相对应的程序代码)
所谓的稳定排序是指如果a原本在b的前面,且a==b,排序之后a仍然在b的前面。
例如无序数组:3(a),5,3(b),2
第一次:2,5,3(b),3(a)
第二次:2,3(b),5,3(a)
第三次:2,3(b),3(a),5
在排序之后,原本相同的数字发生了位置上的转化。
3、选择排序是原地排序
所谓的原地排序是指在排序过程中不申请多余的存储空间,只利用原来存储待排序数据的存储空间进行比较和交换的数据排序。但是允许少量存储空间的使用。
选择排序在排序的过程中需要申请临时存放元素的少量数据空间用以排序过程中的数据交换。
void selectSort(vector<int>&num){
int len = num.size();
int minIndex = 0;
int temp=0;
for(int i=0;i<len-1;i++){
minIndex = i;
for(int j=i+1;j<len;j++){
if(num[j]<num[minIndex])
minIndex = j;
}
temp=num[minIndex];
num[minIndex]=num[i];
num[i]=temp;
}
}
插入排序
- 插入排序是将第一个元素视为有序排列,然后将后面的元素视为将要进行插入的元素,而程序做的事情就是找到将要插入元素应该插入的位置,也就是与有序排列中的最后一个元素进行比较,如果小,则进行元素的移动(当然移动的前提是需要将要进行插入的元素进行记录) ,如果大于有序排列的最后一个元素,则直接进行插入,无需移动。
- 插入排序是一种稳定排序,因为在程序中只有num[j]>key的时候才进行数据的移动,如果两种元素相同则不需要进行移动
- 插入排序是一种原地排序,在比较和交换的时候并不需要申请额外的空间进行排序的辅助工作。
- 插入排序的空间复杂度为最坏的情况下O(n^2),进行数据移动的次数也是O(n^2),空间复杂度为O(1)
void insertSort(vector<int>&num){
//处理特殊情况
if(num.size()<2)
return num;
int len = num.size();
int key=0;
int j=0;
for(int i=1;i < len;i++){
key=num[i]
j=i-1;
while(j>=0&&num[j]>key){
num[j+1]=num[j];
j--;
}
num[j] = key;
}
}
冒泡排序
- 冒泡排序:在每一轮次排序的过程中,都会找到数组待排序元素中的最大值,然后将其交换至数组的末尾,也就是将数组的首部元素与第二个元素进行比较,如果大,则进行交换,否则不交换,然后依次比较相邻的元素,直到最大的元素到达数组的尾部
- 冒泡排序的时间复杂度为O(n^2),空间复杂度为O(1)
- 冒泡排序是稳定排序:因为在比较的过程中,元素比较是以大于为比较的符号,则两个相同元素的位置并不会发生变化。
- 冒泡排序是原地排序:并不需要额外的数组进行存放排序后的数据。需要申请少量的存储空间用以元素的暂存与比较
void bubbleSort(vector<int>&num){
if(num.size()<2)
return num;
int len = num.size();
int temp=0;
for(int i=0;i<len;i++){
for(int j = 0;j<len-i-1;j++){
if(num[j]>num[j+1]){
temp = num[j];
num[j]=num[j+1];
num[j+1]=temp;
}
}
}
}
对上述冒泡排序算法进行优化
即统计是否发生了在某一轮次中并没有出现任何的数据交换,则此时说明元素已经排序完成。
void bubbleSort(vector<int>&num){
if(num.size()<2)
return num;
int len = num.size();
int temp=0;
bool flag = true;
for(int i=0;i<len;i++){
flag = true;
for(int j = 0;j<len-i-1;j++){
if(num[j]>num[j+1]){
flag =false;
temp = num[j];
num[j]=num[j+1];
num[j+1]=temp;
}
}
if(flag)
break;
}
}
希尔排序
- 希尔排序是为了解决直接插入排序中,如果存在较大的值处于前面的位置,这样将会导致很多次的元素移动操作,因此直接插入排序适用于规模较小且部分有序化的数组。
- 希尔排序的思想:将数组进行分割,分割的间隔每一轮次进行相应的变化,第一轮此将间隔设定为最大长度的一半,一轮过后,这样就变成了将较大的元素移动向了后面,虽然有可能将次大一点的元素移动到前面来,但是随着间隔降低,数组中的元素将会呈现局部的有序化,这样,在最后将间隔设定为一的时候又重新变成了直接插入算法,但是此时已经呈现了局部有序化,所以大大降低了元素的移动。
- 希尔排序的时间复杂度:O(nlgn),空间复杂度:O(1).
- 希尔排序是不稳定排序:由于希尔排序是一种跳跃式的排序,这样有可能会导致将相同的元素更换彼此的位置。
- 希尔排序是原地排序,在交换的时候不需要申请额外的空间进行相应的辅助。
void shellSort(vector<int>&num){
if(num.size()<2)
return num;
int len =num.size();
int key=0;
int j=0;
for(int h=len/2;h>0;h/=2){
for(int i=h;i<len;i++){
key=num[i];
j=i-h;
while(j>=0&&num[j]>key){
num[j+h]=num[j];
j-=h;
}
num[j+h]=key;
}
}
}
归并排序
- 采用分治的思想,将待排序的序列进行划分,等到将其划分为每一个序列仅有一个元素的时候,也就保障了该序列的有序性,然后将该序列与其兄弟序列进行合并,然后递归的进行合并,最后合并成一个完整的有序序列。
- 归并排序的时间复杂度:O(nlogn),空间复杂度:O(n)(需要使用一个与待排序序列相同大小的数组用于赋值)
- 归并排序是稳定排序,也就是在进行合并的时候,首先加入辅助数组的是前面的元素。
- 归并排序不是原地排序,在排序的时候需要使用到额外的存储空间。
void merge(vector<int>&num,int left,int mid,int right){
//申请辅助的数组用于存放排序后的数组
vector<int> temp;
int i=left;
int j=mid+1;
while(i<=mid&&j<=right){
if(num[i]<num[j])
temp.push_back(num[i++]);
else
temp.push_back(num[j++]);
}
while(i<=mid)
temp.push_back(num[i++]);
while(j<=right)
temp.push_back(num[j++]);
int len =temp.size();
for(int k=0;k<len;k++){
num[left++]=temp[k];
}
}
void mergeSort(vector<int>&num,int left,int right){
if(left<right){
int mid = (left+right)/2;
num = mergeSort(num,left,mid);
num = mergeSort(num,mid+1,right);
merge(num,left,mid,right);
}
}
快速排序
- 快速排序是归并排序的升级版,其时间复杂度为O(nlogn),其空间复杂度为O(1),非稳定排序,原地排序。
- 归并排序在排序的过程中需要将排序的结果暂存在辅助数组中,并且在排序结束之后,将辅助数组中的元素复制回源数组中,所以归并排序不是原地排序,而快速排序解决了这样的缺点,并不需要辅助数组,同样也就不需要进行复制,原地排序。
- 快速排序是选取一个待排的元素作为排序的基准线,并将所有待排序列中的所有小于基准线元素的元素放置在基准线元素的左边,而所有待排序列中所有大于基准线元素的元素放置在基准线元素的右边,然后将基准线元素置于特定的位置,返回基准线所处的位置用于下一次的划分,左右两边的元素分别按照上述划分的方式进行排序,直到左右两边仅剩一个元素,这样就可以确定局部序列的有序性,同样整体也保证了有序性。
- 快速排序是非稳定排序,因为在划分的时候有可能将基准线元素移动到相同元素的后面。
- 快速排序是原地排序,不需要额外的数组进行暂存。
int partition(vector<int>&arr,int left,int right){
int midValue = arr[left];
int i=left+1;
int j=right;
int temp=0;
while(true){
while(i<=j&&arr[j]>=midValue)j--;
while(i<=j&&arr[i]<=midValue)i++;
if(i>=j)
break;
temp = arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
//为什么这里是与j对应的元素进行交换?因为这里是与前面到来的位置进行交换
//也就是必须与小于基准值的元素进行交换,如果基准值在后面,就需要与i的值进行交换,因为
//i相对应的元素的值要大于基准值。
arr[left]=arr[j];
arr[j] = midValue;
//为什么这里返回的时j的值,而不是i的值,因为在这里返回的应该是下一次的基准值,
//下一次划分的依据就是在左右两边:左边的元素都比基准值小,右边的元素值都比基准值大
return j;
}
void quickSort(vector<int> &arr,int left,int right){
if(left<right){
int mid = partition(arr,left,right);
arr=quickSort(arr,left,mid-1);
arr=quickSort(arr,mid+1,right);
}
}
二叉堆
二叉堆的概念
1、二叉堆具有完全二叉树的特性。
2、堆中的任意一个父节点的值都大于等于它左右孩子节点的值,或者都小于等于它左右节点的值。
二叉堆的分类
如果堆中的任意一个父节点的值都大于等于它左右孩子节点的值则称之为最大堆。
如果堆中的任意一个父节点的值都小于等于它左右孩子节点的值则称之为最小堆
二叉堆的相关性质
二叉堆的根节点称之为堆顶,并且堆顶要么是最大元素,要么是最小元素。
二叉堆的相关操作
1、插入
插入元素首先需要保证给二叉堆的第一个性质也就是说需要满足完全二叉树的性质,所以往往是将元素附在二叉树的最后一个元素后面,这样来维持完全二叉树的形式,同样在插入之后,应该使得插入元素的数值满足二叉堆的第二个位置,进行上浮。
例如:
1
2 3
4 5 6 7
8 9 0
此时插入的元素为0,应该将0与其父节点5进行交换,然后与父节点2进行交换,最后与父节点1进行交换,最终结果为
0
1 3
4 2 6 7
8 9 5
删除
删除操作一般进行删除的是根节点,对根节点进行删除也必须要满足相关二叉堆的概念,删除操作的方式是将完全二叉树的最后一个节点,替代根节点的位置,然后再调整根节点元素的位置,也就是进行下沉操作。
构建
二叉堆的构建,一般二叉树的构建是采用链表的方式进行,而二叉堆是采用数组的方式进行存储,那么标识二叉树父节点以及左右孩子节点的方式就是通过:若父节点的下标为n,则左边孩子节点的下标为2*n+1,而右边孩子节点的下标为2*n+2
以上已经给出了二叉堆进行元素位置调整的两种方式,即:上浮以及下沉。而进行创建的过程就是进行下沉的过程。
vector<int> downAdjust(vector<int>& arr,int parent,int len) {
int temp = arr[parent];//记录父节点的值
//定义左孩子节点的位置
int child = 2 * parent + 1;
while (child < len) {
//如果右孩子节点的值要小于左孩子节点的值,则定位到有孩子节点
if (child + 1 < len && arr[child] > arr[child + 1]) {
child++;
}
//判定此时的父节点值与孩子节点之间最小值的大小关系,如果小于或等于最小值,则下沉结束
if (temp <= arr[child]) {
break;
}
//进行交换
arr[parent] = arr[child];
//进行移动
parent = child;
child = 2 * parent + 1;
}
//最初值移动,在循环中是将子节点向上进行移动,而相应得最初节点并没有停止下沉
arr[parent] = temp;//根节点进行下沉
return arr;
}
vector<int> upAdjust(vector<int>& arr, int len) {
//在执行数据插入的时候执行上浮的程序
int child = len - 1;//完全二叉树得最后一个元素
int parent = (child - 1) / 2;//父节点下标
int temp = arr[child];//记录子节点得值
while (child > 0&& arr[child] < arr[parent]) {
//判定子节点与父节点之间的大小关系,如果父节点的值大于子节点的值,则进行上浮
arr[child] = arr[parent];
child = parent;
parent = (child - 1) / 2;
}
arr[child] = temp;
return arr;
}
void binaryHeap(vector<int>&arr) {
int len = arr.size();
for (int i = (len - 2) / 2; i >= 0; i--) {
downAdjust(arr, i, len);
}
}
堆排序
- 时间复杂度:O(nlogn),空间复杂度:O(1),非稳定排序(因为在构建最大最小堆的时候有可能会对其进行跳跃性排序),原地排序
- 堆排序利用的是二叉堆删除元素的性质进行无序序列的排序。
- 二叉堆删除是将堆顶元素进行删除,然后将最后一个节点元素值当作堆顶元素,并将其下沉处理,所以在二叉堆排序的过程中需要根据数组元素创建二叉堆结构,然后使用二叉堆元素删除的方式进行二叉堆的排序操作(排序需要将二叉堆中的所有元素进行删除),最大堆是从小到大排序,因为堆顶元素最大,这样在删除的时候将其放置在最后的节点中,所以在数组的遍历中,其处于最低位。
//最大堆
void downAdjust(vector<int>&arr,int parent,int len){
//记录父节点的值
int temp = arr[parent];
//利用父节点的下标计算左边孩子节点的下标(定位)
int child = 2 * parent + 1;
//执行下沉操作
while(child<len){
//如果右边节点的值大于左边节点的值,定位到右边节点
if(child+1 < len && arr[child] < arr[child+1])
child++;
//如果父节点的值大于孩子节点中的最大值,则停止下沉
if(temp>=arr[child])
break;
//进行父节点的赋值以及移动
arr[parent]=arr[child];
parent = child;
child = 2 * parent + 1;
}
arr[parent] =temp;
}
void heapSort(vector<int>&arr){
//首先创建最大堆
int len =arr.size();
for(int i=(len-2)/2;i>=0;i--){
downAdjust(arr,i,len);
}
//然后利用二叉堆删除的方式进行排序
int temp = 0;
for(int i= len-1;i>=1;i--){
temp = arr[0];
arr[0]=arr[i];
arr[i] =temp;
downAdjust(arr,0,i);
}
}
统计排序
计数排序
- 计数排序:适用于无序序列中最大值与最小值的差值不是很大的排序。
- 使用一个临时数组对元素出现的次数进行统计,临时数组的大小是最大值+1也就是保证能够将最大值进行囊括,然后遍历临时数组进行数据的输出
- 时间复杂度:在差值不大的情况下要优于o(nlogn),主要是在找最大值的过程中O(n),然后需要遍历临时数组进行输出O(n),初始化临时数组O(n),最终的时间复杂度为O(n+k).
- 空间复杂度0(k)
- 非稳定排序(进行输出的时候并不能够判断相同的元素在无序序列中的前后位置,所以稳定性待商榷)
- 非原地排序(需要临时数组统计无序数组中的元素计数值)
版本一
void countSort(vector<int>&arr){
//遍历找到最大值
int len =arr.size();
int maxValue=0;
for(int i=0;i<len;i++){
if(arr[i]>maxValue){
maxValue = arr[i];
}
}
//统计元素出现次数
vector<int> temp(maxValue+1,0);
for(int i=0;i<len;i++){
temp[arr[i]]++;
}
//将辅助数组中的元素复制回原数组
int k=0;
for(int i=0;i<maxValue+1;i++){
for(int j=0;j<temp[i];j++){
arr[k++]=i;
}
}
}
版本二:
传统的计数排序需要辅助数组进行存放统计元素的个数,但是由于最小值有可能不是0,而是一个相当大的数,虽然最大值与最小值之间的差距不大,但是从0开始出发,这样将会导致极大的空间浪费问题。
所以优化后的计数排序,采用最小值作为辅助空间的开始,并以最小值作为偏移量进行数组的初始化操作。
void greatCountSort(vector<int>&arr){
//记录最大最小值
int len =arr.size();
int maxValue=0;
int minValue=0;
for(int i=0;i<len;i++){
maxValue=max(maxValue,arr[i]);
minValue=min(minValue,arr[i]);
}
//申请辅助数组进行存放
vector<int>temp(maxValue-minValue+1,0);
//统计计数信息
for(int i=0;i<len;i++){
temp[arr[i]-minValue]++;
}
int k=0;
for(int i=0;i<maxValue-minValue+1;i++){
for(int j=0;j<temp[i];j++){
arr[k++]=i+minValue;
}
}
}
版本三:
版本二中虽然进行了一定程度的优化,但是对于某些实际应用场景不太符合,也就是说计数值不唯一的情况下,不满足稳定性排序的 要求,版本三做到稳定排序。
vector<int> greatAgainCountSort(vector<int>& arr) {
int len = arr.size();
int minValue = arr[0];
int maxValue = arr[0];
for (int i = 1; i < len; i++) {
if (arr[i] > maxValue) maxValue = arr[i];
else if (arr[i] < minValue) minValue = arr[i];
}
//申请数组用以存放统计信息
int dis = maxValue - minValue;
vector<int>temp(dis + 1, 0);
for (int i = 0; i < len; i++) {
temp[arr[i] - minValue]++;
}
//对上述统计信息进行格式化处理
for (int i = 1; i < dis + 1; i++) {
temp[i] += temp[i - 1];
}
//申请数组用以存放格式化之后的统计信息
vector<int>sortedArray(len, 0);
for (int i = len-1; i >=0; i--) {
sortedArray[temp[arr[i] - minValue]-1] = arr[i];
temp[arr[i]-minValue]--;
}
return sortedArray;
}
桶排序
- 从总体上来讲,桶排序是计数排序的升级版,能够解决计数排序中数据所处范围过大(就是最大值与最小值的差值太大)以及序列元素不是整数的情况,同样计数排序也可以说是桶排序的一种特例,即计数区间为1,桶的个数为n+1(n为最大值与最小值的差值)。
- 桶排序能够自定义桶的个数bucketNum,本文采用的方式为bucketNum =(maxValue-minValue)/len +1;而此时第i个桶的表达范围为:minValue+len*i ~ minValue +len*(i+1),此时必定存在区间的开闭问题,所以我们使用左闭右开的原则,也就是最后一个桶只有一个元素就是最大值元素。这也是加1的由来,
- 遍历无序序列然后判定序列中元素应该进入桶的位置,然后对于每一个桶执行插入排序进行排序(或者其他方式的排序),最后在进行所有桶中的元素的集合。
- 桶排序是稳定排序,以及非原地排序
- 时间复杂度为O(n+k),空间复杂度为O(n+k)
- 桶排序的局限性,适用于待排序元素分布均匀的情况,如果元素大多都集中在一个桶中,这样将会退化为对桶中元素进行排序的算法,并且还创建了众多的空桶。
void bucketSort(vector<int>&arr){
int len =arr.size();
int minValue =arr[0];
int maxValue =arr[0];
//寻找序列中的最大最小值
for(int i=0;i<len;i++){
if(arr[i]>maxValue){
maxValue=arr[i];
}
else if(arr[i]<minValue){
minValue = arr[i];
}
}
//确定桶的个数(算法并不唯一,可以自定义指定,此处设定为桶的个数为bucket = (maxValue-minValue)/len + 1)
int bucketNum = (maxValue-minValue)/len +1;
/*新建存放链表的动态数组,为什么使用链表进行存放桶中的数据,而不是使用动态数组(整体为二维动
态数组进行存放)?因为并不清楚每一个桶中的元素具体有多少个,当然自我感觉也可以使用push_back
方法进行创建,并且排序的时候也是需要讲所有元素入桶之后再进行排序,而算法导论中的使用插入排序
进行模拟入桶时的过程,仅适用于人类去算吧,如果每加入一个元素就调用插入排序进行排序,仅仅调用
函数的开销就挺大吧,还不如直接使用快速排序进行总体桶中元素的排序操作。*/
vector<list<int>>bucket;
for(int i=0;i<bucketNum;i++){
bucket.push_back(list<int>());
}
//将元素入桶
//注意此时元素的定桶方式
for(int i=0;i<len;i++){
bucket[(arr[i]-minValue)/len].push_back(arr[i]-minValue);
}
//将桶中元素进行排序
for(int i=0;i<bucketNum;i++){
bucket[i].sort();//采用list中自带的排序算法进行桶中元素的排序,注意STL容器中如果不支持
//随机访问,则不能够使用algorithm中的sort方法,并且再容器的定义中一定会有sort方法的重载。
}
//将桶中元素进行重新组合
int k=0;
for(int i=0;i<bucketNum;i++){
for(list<int>::iterator it=bucket[i].begin();it!=bucket[i].end();it++){
arr[k++]=*it+minValue;
}
}
}
基数排序
同样是一种时间复杂度为O(n)的排序算法,其基于桶排序进行设计,主要的思想就是首先进行个位数的排序,然后进行十位数的排序,一直到最大值的位数比较完成,从而完成整体的排序工作。
个位取值范围共有10个,也就是每一次位数的排序将会分为10个桶,相同位数上的数字进入相应的桶中。然后按照次序将其放回原数组,然后再执行十位数的桶排序,依次进行排序。
时间复杂度:O(kn)
空间复杂度:O(n+k)
稳定排序
非原地排序
void redixSort(vector<int>& arr) {
int len = arr.size();
int maxValue = arr[0];
//找到最大值
for (int i = 1; i < len; i++) {
if (arr[i] > maxValue)
maxValue = arr[i];
}
//判定最大值的位数
int count = 0;
while (maxValue != 0) {
count++;
maxValue /= 10;
}
//申请存放桶的辅助空间
vector<list<int>>bucket(10);
for (int i = 0; i < count; i++) {
//将无序数组中的元素入桶
int digit = 0;
for (int j = 0; j < len; j++) {
digit = (arr[j] / (int)pow(10, i)) % 10;
bucket[digit].push_back(arr[j]);
}
//将入桶的元素再重新回到源数组中
int k = 0;
for (int j = 0; j < 10; j++) {
for (list<int>::iterator it = bucket[j].begin(); it != bucket[j].end(); it++) {
arr[k++] = *it;
}
bucket[j].clear();
}
}
}