数据结构与算法——排序

目录

1.排序的概念及运用

2.常见的排序算法的实现

3.排序算法复杂度及稳定性


1.排序的概念及运用

1.1排序的概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

稳定性:即在记录中的某些关键字在排序前和排序后位置保持相对稳定。

内部排序:数据元素全部放在内存中的排序。

外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不断的在内外存之间移动数据的排序。

1.2排序运用

1.3常见的排序算法

各个排序动态演示效果

//排序实现的接口

//向下调整

void AdjustDown(int *a,int n,int parent);

//堆排序

void HPSort(int *a,int n);

//直接插入排序

void InsertSort(int *a,int n);

//希尔排序

void ShellSort(int *a,int n);

//选择排序

void SelectSort(int *a, int n) ;

//快排霍尔法

int QSPart1(int *a,int left,int right);

//快排前后指针法,与霍尔法相比效率略低

int QSPart2(int *a,int left,int right);

//快速排序

void QuickSort(int *a,int left,int right);

//非递归快排

void QuickSortNonr(int *a,int left,int right);

//冒泡排序

void BubbleSort(int *a,int n);

void _MergeSort(int *a,int *tmp,int left,int right);

//归并排序

void MergeSort(int *a,int n);

//非递归归并

void MergeSortNonr(int *a,int n);

//计数排序

void CountSort(int *a,int n);

2.常见的排序算法的实现

2.1插入排序

2.1.1基本思想:

直接插入排序是一种简单的插入排序法,其基本思想是:

把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列

现实中我们打扑克的时候就用到了插入排序

2.1.2直接插入排序

当插入第i(i>=1)个元素时,前面的array [0],array[1]...,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2]...的排序码顺序进行比较,找到插入位置即将array[i]插入,原来的位置发生后移

//直接插入排序

void InsertSort(int *a,int n){

for(int i=0;i<n-1;i++){

int end=i;

int tmp=a[end+1];

while(end>=0){

if(tmp<a[end]){

a[end+1]=a[end];

end--;

}

else break;

}

a[end+1]=tmp;

}

}

直接插入排序的特性总结:

1.元素集合越接近有序,直接插入排序算法时间效率越高

2.时间复杂度:O(N^2)

3.空间复杂度:O(1),它是一种稳定的排序算法

4.稳定性:稳定

2.1.3希尔排序(缩小增量排序)

希尔排序又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成gap组,所有距离相同的记录分在同一组内,并对每一组内的记录进行排序。然后重复上述分组和排序的工作。当gap到达1时,所有记录在统一组内排好序

//希尔排序(1.预排序 2.直接插入)时间复杂度:O(N^1.3)

void ShellSort(int *a,int n){

// int gap=3;

// 第一种预排序,gap组分别排序

// for(int j=0;j<gap;j++){

// for(int i=j;i<n-gap;i+=gap){//对每一组排序

// int end=i;

// int tmp=a[end+gap];

// while(end>=0){

// if(tmp<a[end]){

// a[end+gap]=a[end];

// end-=gap;

// }

// else break;

// }

// a[end+gap]=tmp;

// }

// }

//第二种预排序,gap组一起排序,各组互不影响

// for(int i=0;i<n-gap;i++){

// int end=i;

// int tmp=a[end+gap];

// while(end>=0){

// if(tmp<a[end]){

// a[end+gap]=a[end];

// end-=gap;

// }

// else break;

// }

// a[end+gap]=tmp;

// }

//最终版本,第二步不直接调用直接插入排序,而是动态控制gap完成2个步骤

int gap=n;

while(gap>1){

gap=gap/3+1;//gap大于1时进行预排序,等于1时直接插入排序

//最后一个gap一定等于1

for(int i=0;i<n-gap;i++){//gap组并行排序

int end=i;

int tmp=a[end+gap];

while(end>=0){

if(tmp<a[end]){

a[end+gap]=a[end];

end-=gap;

}

else break;

}

a[end+gap]=tmp;

}

}

}

希尔排序的特性总结:

1.希尔排序是对直接插入排序的优化

2.当gap大于1时都是预排序,目的是让数组更接近有序。当gap为1时,数组接近有序,这样就更快,对整体而言,可以达到优化的效果

3.时间复杂度:约为O(N^1.3)

4.稳定性:不稳定

2.2选择排序

2.2.1基本思想

每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在待排序列的起始位置,直到全部待排序的数据元素排完

2.2.2直接选择排序

●在元素集合array[i]--array[n-1]中选择关键码最小(大)的数据元素

●若它不是这组中最后一个(第一个)元素,则将它与这组元素中最后一个(第一个)元素交换

●在剩余的arrray[i]--array[n-2](array[i+1]--array[n-1]集合中,重复上述步骤,直到集合剩一个元素为止

//选择排序

void SelectSort(int *a, int n) {

//优化,一次选出最大和最小

int begin = 0, end = n - 1;

while (begin < end) {

int maxi = begin, mini = begin;

for (int i = begin + 1; i <= end; i++) {

if (a[i] > a[maxi]) maxi = i;

if (a[i] < a[mini]) mini = i;

}

// 将最小元素放到begin位置

swap(a[begin], a[mini]);

// 如果最大值的索引和之前放置最小值的索引相等,

// 因为之前交换了位置,需要更新maxi的索引

if (maxi == begin)

maxi = mini;

// 将最大元素放到end位置

swap(a[end], a[maxi]);

begin++;

end--;

}

}

直接选择排序的特性总结

1.直接选择排序非常好理解,但效率低下。实际很少使用

2.时间复杂度:O(N^2)

3.空间复杂度:O(1)

4.稳定性:不稳定(例如6 6 1 6 )

2.2.3堆排序

堆排序是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。注:排升序建大堆,排降序建小堆

//向上调整

void AdjustUp(int *a,int child){

int parent=(child-1)/2;

//两种情况退出:1.从最下面调整到根 2.孩子与父亲已经有序

while(child){

if(a[parent]>a[child]){//建大堆

swap(a[parent],a[child]);

child=parent;

parent=(child-1)/2;

}

else break;

}

}

//向下调整

void AdjustDown(int *a,int n,int parent){

int child=parent*2+1;//假设左孩子为小

//依旧两种情况退出

while(child<n){

//右孩子必须存在

if(child+1<n&&a[child+1]>a[child]) child++;

if(a[child]>a[parent]){//建大堆

swap(a[child],a[parent]);

parent=child;

child=parent*2+1;

}

else break;

}

}

//堆排序

void HPSort(int *a,int n){

//向上调整,时间O(N*logN)

//for(int i=1;i<n;i++) AdjustUp(a,i);

//向下调整,时间O(N),从倒数第一个非叶子节点开始

for(int i=(n-1-1)/2;i>=0;i--) AdjustDown(a,n,i);

//O(N*logN)

int end=n-1;

while(end){

swap(a[0],a[end]);

AdjustDown(a,end,0);//向下调整中,child无法等于end

--end;

}

}

堆排序的特性总结:

1.堆排序使用堆来选数,效率较高

2.时间复杂度:O(N*logN)

3.空间复杂度:O(1)

4.稳定性:不稳定

2.3交换排序

基本思想:所谓交换就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列尾部移动,键值较小的记录向序列的前部移动。

2.3.1冒泡排序

//冒泡排序

void Bubble(int *a,int n){

for(int i=0;i<n;i++){

for(int j=i;j+1<n;j++){

if(a[j]>a[j+1]{

int t=a[j];

a[j]=a[j+1];

a[j+1]=t;

}

}

}

}

冒泡排序的特性总结:

1.冒泡排序是一种非常容易理解的排序

2.时间复杂度:O(N^2)

3.空间复杂度O(1)

4.稳定性稳定

2.3.2快速排序

快速排序是Hoare在1962提出的一种二叉树结构的交换排序方法,基本思想:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应的位置上为止

void QuickSort(int *a,int left,int right){

if(left>=right) return;

int keyi=QSPart1(a,left,right);

//继续分割

QuickSort(a,left,keyi-1);//左

QuickSort(a,keyi+1,right);//右

}

1.Hoare法

//快排霍尔法

int QSPart1(int *a,int left,int right){

//三数取中

int keyi=GetMidl(a,left,right);//选一key,左边找大,右边找小,找到交换,直到相遇

swap(a[left],a[keyi]);

int begin=left,end=right;//left不能变,防止找不到

while(begin<end){

//右边先走,到相遇时就是左边找右边。

//若左边先走,前面交换位置相同,到找相遇时会有影响,将比关键字大的数错误的交换到了前面

//所以要想正确,若左边先走,应将右边设为关键字,相遇时右边找左边

while(begin<end&&a[end]>=a[keyi]){//右边找小

--end;

}

while(begin<end&&a[begin]<=a[keyi]){//左边找大

++begin;

}

swap(a[begin],a[end]);

}

swap(a[keyi],a[begin]);

return begin;//左右分割,此时左边都是小于keyi的数,右边都是大于它的数

}

2.挖坑法

3.前后指针法

//快排前后指针法,与霍尔法相比效率略低

int QSPart2(int *a,int left,int right){

//两指针,依然cur找小,找到后,prev++,交换,当cur越界后,prev与keyi交换

//此时,keyi左边都是小于keyi的数,右边都是大于它的数

int prev=left,cur=prev+1;

int keyi=GetMidl(a,left,right);

swap(a[left],a[keyi]);

while(cur<right){

if(a[cur]<a[keyi]&&++prev!=cur){//当两指针指向同一位置时,不用交换,无意义

swap(a[cur],a[prev]);

}

cur++;

}

swap(a[prev],a[keyi]);

return prev;

}

2.3.3快速排序的优化(面试时不用写出,讲解思路即可)

1.三数取中法选key

2.递归到小的子区间时,依然递归没有必要,可以考虑使用插入排序

//三数取中

int GetMidl(int *a,int left,int right){

int mid=(left+right)/2;

if(a[left]<a[mid]){

if(a[mid]<a[right]){

return mid;

}

else if(a[left]<a[right])

return right;

else return left;

}

else {

if(a[mid]>a[right]){

return mid;

}

else if(a[left]<a[right])

return left;

else return right;

}

}

//快速排序

//当序列为有序时,效率降低

//改进:1.随机取key 2.三数取中

void QuickSort(int *a,int left,int right){

if(left>=right) return;

//当left差距较小时,用QuickSort递归太浪费,小区间优化

if(right-left<10){

InsertSort(a+left,right-left+1);

}

else{

int keyi=QSPart1(a,left,right);

//继续分割

QuickSort(a,left,keyi-1);//左

QuickSort(a,keyi+1,right);//右

}

}

2.3.4非递归的快速排序

//非递归快排

void QuickSortNonr(int *a,int left,int right){

//利用栈将分割的左右区间存储,由于后进先出的结构,先压右半区间,在压左半区间

//当栈空时,排序完毕。非递归还是采用递归的思想

ST st;

STInit(&st);

STPush(&st,rifht);

STPush(&st,left);

while(!STEmpty(&st)){//无限分割,当栈空分割完毕

int begin=STTop(&st);

STPop(&st);

int end=STTop(&st);

STPop(&st);

int keyi=QSPart1(a,begin,end);

//分割序列

if(keyi+1<end){//压栈,先右区间,后左区间

STPush(&st,end);

STPush(&st,keyi+1);

}

if(keyi-1>begin){

STPush(&st,keyi-1);

STPush(&st,begin);

}

}

STDestory(&st);

}

快速排序的特征总结

1.快速排序整体的综合性能和使用场景较好

2.时间复杂度O(N*logN)

3.空间复杂度O(logN)

4.稳定性:不稳定

2.4归并排序

2.4.1基本思想:

归并排序是建立在归并操作上的一种有效排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

void _MergeSort(int *a,int *tmp,int left,int right){

if(left>=right) return ;

int mid=(left+right)/2;//中间分割序列

//[left,mid] [mid+1,right],归并

_MergeSort(a,tmp,left,mid);

_MergeSort(a,tmp,mid+1,right);

int begin1=left,end1=mid;

int begin2=mid+1,end2=right;

int j=left;

while(begin1<=end1&&begin2<=end2){//tmp中排序

if(a[begin1]<a[begin2]) tmp[j++]=a[begin1++];

else tmp[j++]=a[begin2++];

}

//将剩余全部排入tmp

while(begin1<=end1) tmp[j++]=a[begin1++];

while(begin2<=end2) tmp[j++]=a[begin2++];

memcpy(a+left,tmp+left,sizeof(int)*(right-left+1));

}

//归并排序,时间复杂度O(NlogN)

void MergeSort(int *a,int n){

int *tmp=(int *)malloc(sizeof(int)*n);

if(!tmp){

perror("malloc fail");

return;

}

_MergeSort(a,tmp,0,n-1);

free(tmp);

tmp=NULL;

}

2.4.2非递归归并排序

//非递归归并

void MergeSortNonr(int *a,int n){

int *tmp=(int *)malloc(sizeof(int)*n);

if(!tmp){

perror("malloc fail");

return;

}

//每组归并数据的数据个数

int gap=1;

while(gap<n){

//分别归并

for(int i=0;i<n;i+=2*gap){

int begin1=i,end1=i+gap-1;

int begin2=i+gap,end2=i+2*gap-1;

//[begin1,end1] [begin2,end2]

//3种越界。1.只有end2越界 2.begin2,end2越界 3.end1,begin2,end2越界

//若第二组都越界,直接跳出

if(begin2>=n) break;

//若end2越界,修改

if(end2>=n) end2=n-1;

int j=i;

while(begin1<=end1&&begin2<=end2){//tmp中排序

if(a[begin1]<a[begin2]) tmp[j++]=a[begin1++];

else tmp[j++]=a[begin2++];

}

//将剩余全部排入tmp

while(begin1<=end1) tmp[j++]=a[begin1++];

while(begin2<=end2) tmp[j++]=a[begin2++];

memcpy(a+i,tmp+i,sizeof(int)*(end2-i+1));

}

gap*=2;

}

free(tmp);

tmp=NULL;

}

归并排序的特性总结:

1.归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是在磁盘中的外排序问题

2.时间复杂度:O(N*logN)

3.空间复杂度:O(N)

4.稳定性:稳定

2.5非比较排序

思想:计数排序又称鸽巢原理,是对哈希直接定址的变形应用。桶思想

1.统计相同元素出现次数

2.根据统计的结果将序列回收到原来的序列中

//计数排序

void CountSort(int *a,int n){

int max=a[0],min=a[0];

for(int i=1;i<n;i++){

if(a[i]<min) min=a[i];

if(a[i]>max) max=a[i];

}

//将要开的数组空间

int range=max-min+1;

int *count=(int*)calloc(range,sizeof(int));

if(count==NULL){

perror("calloc fail");

return ;

}

//相对定位,减少空间浪费

for(int i=0;i<n;i++){

count[a[i]-min]++;

}

int j=0;//直接原数组覆盖

for(int i=0;i<range;i++){

while(count[i]--){

a[j++]=i+min;

}

}

}

计数排序的特性总结:

1.计数排序在数据范围集中时,效率很高,但是适用范围及场景有限

2.时间复杂度:O(MAX(N范围))

3.空间复杂度:O(范围)

4.稳定性:稳定

3.排序算法复杂度及稳定性

相关推荐
roman_日积跬步-终至千里40 分钟前
【模式识别与机器学习(14)】K-means算法中K值确定教程
算法·机器学习·kmeans
CQ_YM43 分钟前
数据结构之双向链表
数据结构·链表
一只乔哇噻44 分钟前
java后端工程师+AI大模型进修ing(研一版‖day59)
java·开发语言·算法·语言模型
天赐学c语言44 分钟前
12.2 - LRU缓存 && C语言内存布局
c++·算法·lru·内存布局
秋深枫叶红1 小时前
嵌入式第二十六篇——数据结构双向链表
c语言·数据结构·学习·链表
_OP_CHEN1 小时前
【算法基础篇】(二十三)数据结构之并查集基础:从原理到实战,一篇吃透!
数据结构·算法·蓝桥杯·并查集·算法竞赛·acm/icpc·双亲表示法
liu****1 小时前
10.指针详解(六)
c语言·开发语言·数据结构·c++·算法
CQ_YM1 小时前
数据结构概念与顺序表
数据结构·算法·线性表