- 关键字的取值情况,会使排序结果可能不唯一。
- 按照主关键字排序,排序结果唯一;按照次关键字排序,排序结果可能不唯一。
- 若对任意的数据元素序列,使用某个排序方法,对它按关键字进行排序:若相同关键字元素间的位置关系,排序前与排序后保持一致,称此排序方法是稳定的;而不能保持一致的排序方法则称为不稳定的。
typedef struct {
int r[Max];
int length;
}SqList;
SqList L;
一、插入排序
(一)直接插入排序
思路讲解:【数据结构】算法题 直接插入排序_哔哩哔哩_bilibili
//待排序的数据元素从下标为1的数组元素开始存放。下标0处为哨兵项
void insertSort(SqList L) {
int i, j;
for(i = 2; i <= L.length; i++)//从第二个数据开始插入,n = L.length
if(L.r[i].key < L.r[i-1].key) {//第i个数据比前面已经有序的i-1个数据最大的小
L.r[0] = L.r[i];//将第i个数据放入哨兵位置
L[i] = L.r[i-1];
for(j = i-2; L.r[0].key < L.r[j].key; --j)///L.r[0]存放的是此次要插入的第i个数据
L.r[j+1] = L.r[j];
L.r[j+1] = L.r[0];
}
}
- 直接插入排序是一个稳定的排序方法
- 平均时间复杂度为:O(n2),
- 一个额外的辅助空间, O(1) 。
- 对于有n个数据元素的待排序序列,插入操作要进行n-1趟。
- 最好情况: n-1次数据比较,0次数据移动。待排序的数据基本有序,时间复杂度O(n)
- 最坏情况:待排序的数据逆序。 i 比较次数 移动次数 2 2 3 3 3 4 …… n n n+1
- 数据比较次数=(n+2)(n-1)/2
- 数据移动次数=(n+4)(n-1)/2
- 直接插入排序,待排序的数据用数组和链表存放均可
- 可证平均时间复杂度约为:n^2/4。当n<16时, n^/4<nlogn。理论值当n<16时,直接插入比O(nlogn)的排序方法快!
(二)二分插入排序
void BinsertSort(SqList &L) {
int i, low, high, mid;
for(i = 2; i <= L.length; i++)
if(L.r[i].key < L.r[i-1].key) {
L.r[0] = L.r[i];
low = 1;
high = i-1;
while(low <= high) {
mid = (low +high)/2;
if(L.r[0].key < L.r[mid].key)high = mid - 1;
else low = mid +1;
}
for(j = i-1; k >= high + 1; j--)
L.r[j+1] = L.r[j];
L.r[high + 1] = L.r[0];
}
}
- 二分插入排序减少了关键字的比较次数,但数据元素的移动次数不变,其时间复杂度与直接插入排序相同。
- 时间复杂度:O(n^2)
- 待排序的数据元素必须存放于数组
(三)希尔排序
思路:插入排序——希尔排序_看不懂你打我系列_哔哩哔哩_bilibili
性能分析:待排序数据:7,7,4
我们表示为:7(1) ,7(2) ,4
第一趟(d1=2):4,7(2) ,7(1)
第二趟(d2=1):4,7(2) ,7(1)
关键字值相同的数据元素7在排序前后的相对位置发生了变化,所以希尔排序是不稳定的排序方法
二、交换排序
(一)快速排序
int partition(SqList L, int l, int h) {
L.r[0] = L.r[l];//取待排序的第一个数据元素为基准放到L.r[0]
//原来的L.r[l]初始相当于空着
//从后面开始找
while(l < h) {//一趟快排结束的条件是左右边界重合
//若右边界大于基准,则右边界左移一位
while((l<h) && (L.r[h].key >= L.r[0].key))
h--;
//右边界小于基准,则把右边的大数放到前面的空位
if(l < h) {
L.r[l] = L. r[h];
l++;
}
//若左边界小于基准,则左边界右移一位
while((l<h) && (L.r[h].key >= L.r[0].key))
l++;
//若大于,则把左边的小数移到后面的空里
if(l < h) {
L.r[h] = L. r[l];
h--;
}
//l=h跳出循环
L.r[l] = L.r[0];//把基准元素放到这个重合处的空里
return l;//返回基准的位置
}
}
//递归,结束的条件是待排序的数据元素个数小于等于1
void QSort(SqList &L. int l, int h) {
int t;
if(l<h) { //待排序的数据有2个或2个以上才进行排序操作
t = partition(L, l, h);//调用一趟快排,t为返回的基准的位置
QSort(L, l, t-1);//对比基准小的子序列继续进行快速排序
QSort(L, t+1, h);//对比基准大的子序列继续进行快速排序
}
}
稳定性:不稳定
例:待排序的数据:7,4,4----》7,4(1),4(2)
第一趟快排:4(2),4(1) , 7
第二趟快排:4(2),4(1) , 7
关键字值相同的数据元素4在排序前后的相对位置发生了变化
平均时间复杂度为O(nlog(2,n))
最坏情况的时间复杂度为O(n^2)
最坏情况待排序的记录基本有序:正序(递增)、逆序(递减)
正序:比较次数总计n(n-1)/2
(二)冒泡排序
- 将待排序的数据元素的关键字顺次两两比较,若为逆序(前大后小)则将两个数据元素交换
- 将序列照此方法从头到尾处理一遍称作一趟冒泡排序,它将关键字值最大的数据元素交换到排序的最终位置
- 对n个元素排序最多需要n-1趟冒泡排序
- 结束条件:做完n-1趟,或:某趟冒泡排序过程中一次数据移动也没发生,说所有数据已经有序,冒泡排序结束
- 最好情况:n个数据元素,1趟冒泡排序,0次数据移动,n-1次比较(初始的待排序序列恰好是递增有序,R1≤R2 ≤…… ≤Rn)
- 最坏情况: n个数据元素, n-1趟冒泡排序。第1趟比较n-1次,移动3(n-1)次,……总计n-1趟,比较n(n-1)/2次,移动3n(n-1)/2次(初始的待排序序列恰好是逆序 ,R1≥R2 ≥ …… ≥ Rn)
- 平均时间复杂度O(n^2)
- 一个额外的辅助空间O(1)
- 冒泡排序是稳定的排序方法
步骤:
第一趟:第1个与第2个比较,第1个大则交换;第2个与第3个比较,第2个大则交换,……,n-1次比较后,关键字最大的数据元素交换到最后一个位置上,此时最后一个元素有序
第二趟:对前n-1个数据元素进行同样的操作,n-2次比较后,关键字次大的数据元素交换到第n-1个位置上;
依次类推,则完成排序。
void qppx(SqList &L) {
int i, j, k;
j = 1;
k = 1;
//共有n-1趟排序:j从1到length-1
while((j < L.length) && (k > 0)) {
k = 0;
//第j趟要比较n-j次:i从1到n-j
for(i = 1; i <= L.length-j; i++)
if(L.r[i+1].key < L.r[i].key]) {//出现逆序,利用辅助空间r[0]交换
L.r[0] = L.r[i];
L.r[i] = L.r[i+1];
L.r[i+1] = L.r[0];
k++;//k记录每一趟排序中发生的交换次数,交换则+1
}
j++;}
}
三、选择排序
(一)简单选择排序
步骤:
n个数据元素进行n-1趟扫描
第1趟扫描:进行n-1次比较,选出n个数据元素中关键字值最小的数据元素,与第1个数据元素交换;
第i趟扫描:进行(n-i)次比较,选出剩下的n-i+1个数据元素中关键字值最小的数据元素,并与第i个数据元素交换;
- 时间复杂度为O(n2)
- 最好、最坏、平均时间复杂度都是O(n^2)
- 不论输入的待排序的数据是什么顺序(最好、最坏都一样),每一趟简单选择排序的比较次数不变,总的比较次数为:(n-1)+(n-2)+…+2+1=n(n-1)/2次
- 最好情况:第一趟找到的最小的恰好在第一个位置,不发生数据交换,…, 每一趟找到的最小的数据都不要交换,输入的待排序的数据恰好有序,总的数据移动次数为0次。
- 最坏情况:第一趟找到的最小的要交换到第一个位置,数据移动3次,…, 每一趟找到的最小的都要交换,数据移动3次。总共n-1趟,总的数据移动次数3(n-1)次。
- 适用于待排序元素较少的情况
- 不稳定
例:待排序的数据:2,2,1----》2(1),2(2) ,1
第一趟简单选择排序:1,2(2) ,2(1)
第二趟简单选择排序: 1,2(2) ,2(1)
关键字值相同的数据元素2在排序前后的相对位置发生了变化
void SelectSort(SqList &L) {
int i, j, k;
for(i = 1; i < L.length; i++) {//进行n-1趟排序
k = i;//第i趟时,初始设当前最小的数据为第k=i个数据元素
for(j = i+1; j <= L.length; ++j)
//从第i+1个到第n个,依次和当前最小的第k个数据元素比较,若比第k个还小,则更新k
if(L.r[j].key < L.r[k].key)
k = j;
if(k != i) {
//交换借用额外的辅助空间L.r[0], O(1)
L.r[0] = L.r[i];
L.r[i] = L.r[k];
L.r[k] = L.r[0];
}
}
}
}
树排序
树排序将时间复杂度降为O(nlogn)但需要的辅助空间增加
步骤看ch10-3ppt,很清晰
(二)堆排序
步骤:
- 以大顶堆为例,对一组待排序的数据元素,将它们建成一个大顶堆(称为初建堆),关键字值最大的数据为堆序列的第一个数据。(筛选法/插入法)n个数据建堆从n/2个数据开始进行调整,即从下到上、从右至左找到第一个非叶结点开始进行调整。
- 将关键字值最大的数据取出(通常与尚未排序的最后一个数据交换存储位置)
- 用剩下的数据元素重建堆(称为一次调整),便得到关键字值次大的数据元素
- 如此反复,直到全部数据排好序
- 时间复杂度为O(nlog(2,n))
- 适用于待排序元素较多的情况
- 一个额外的辅助空间O(1)
- 不稳定。待排序的数据:2,2,1----》2(1),2(2) ,1
void HeapSort(SqList &L) {
int i, j, k;
for(i = L.Length/2; i>0; --i)//筛选法建堆,从n/2处开始调整
HeapAdjust(L, i, L.length); //调整以i为根结点的子树为一个大顶堆
for(i = L.Length; i>1; --i) {
//n-1趟堆 排序,当前大顶堆中的数据元素i个,L.r[1]中是i个数据元素中的最大值
L.r[0] = L.r[i];
L.r[i] = L.r[1];
L.r[1] = L.r[0];
//将堆中最大的数据元素L.r[1]交换到第i个位置,也是它最终排序后的位置
HeapAdjust(L, 1, i-1);
//堆中数据元素个数为i-1,将i-1个数据元素重新调整为大顶堆
}
}
//函数HeapAdjust(L,i,L.Length)-调整以i为根结点的子树为一个大顶堆
//调整以s为根结点的子树为一个大顶堆 ,堆中最大的数据元素编号为m,且以s为根的子树中除根结点s外,均满足大顶堆的定义
void HeapAdjust(SqList &L, int s, int m) {
int j;
L.r[0] = L.r[s];
for(j = 2*s, j <= m; j = j*2) {
if(j < m && L.r[j].key<L.r[j+1].key) ++j;//j为左、右孩子中最大的那个
if(L.r[0].key >= L.r[j].key) break;//
L.r[s] = L.r[j];
s = j;
}
L.r[s] = L.r[0];
}
四、归并排序
基本思想:
- 将待排序序列划分成若干有序子序列
- 将两个或两个以上的有序子序列 “合并” 为一个有序序列
- 通常采用2-路归并排序,即:将两个位置相邻的有序子序列“合并” 为一个有序序列
算法分析:
- 一趟归并的时间复杂度为 O(n)
- 总共需进行log(2,n)趟
- n个记录进行归并排序的时间复杂度为Ο(nlogn)
- 稳定
子序列确定方法:
1.自顶向下:若待排序的序列包含多于1个数据元素,则将其一分为二2.自底向上:待排序的每个数据元素为一个子序列
void Merge(int SR[], int &TR[], int s, int m, int t) {
// 将有序的序列 SR[s..m] 和 SR[m+1..t]归并为有序的序列 TR[s..t]
for(i = s, j = m+1, k = s; i <= m && j <= t; ++k) {
//两个序列的同位置元素相比,小的先放
if(SR[i].key <= SR[j].key)
TR[k] = SR[i++];
// k为合并后的有序序列 TR[s..t]的存放位置,第一个位置为s
else TR[k] = SR[j++];
}
//第一个有序序列 还有数据没有比较,将其复制到合并后的序列;
if(i <= m)
for(; i <= m; )
TR[k++] = SR[i++];
//第二个有序序列 还有数据没有比较,将其复制到合并后的序列;
if(j <= t)
for(; j <= t; )
TR[k++] = SR[j++];
}
}
void Mergesort(SqList &L) {
MSort(L.r, L.r, 1, L.length);
}
void Msort(int SR[], int &TR1[], int s, int t) {
//将SR[s..t] 归并排序为 TR1[s..t]
if(s == t) TR1[s] = SR[s];//序列中只有一个数据元素,序列自然有序
else { //序列中包含2个及以上元素
m = (s+t)/2; //计算序列的中间位置,以此为界划分为2个序列
//将待排序的数据元素序列划分为:SR[s...(s+t)/2]和SR[(s+t)/2 + 1...t]
Msort(SR, TR2, s, m);//对第一个子序列递归调用归并排序算法,使其有序
Msort(SR, TR2, m+1, t);//对第二个子序列递归调用归并排序算法,使其有序
Merge(TR2, TR1, s, m, t);//将2个有序子序列合并为一个有序序列
}
}
五、基数排序
(一)桶排序
基本操作:
- 分配关键字:根据每个数据元素关键字的值将其分配到相应的桶中
- 每个“桶”内排序 数据元素均匀分布,则每个桶中数据元素个数均匀
- 收集桶
- 不能x像比较排序那样以统一的数据元素之间的“比较”次数衡量“工作量
两种排序方法:
- 最高位优先MSD法;
- 最低位优先LSD法:先对Kd-1进行分配排序,再对Kd-2 进行分配排序,……, 依次类推,直至最后对最高位关键字分配排序完成为止
通常采用低位优先——简单方便
下面介绍低位优先法
************这个代码不懂
#define MAX_NUM_OF_KEY 8//关键字个数最大值
#define radix 10//队列个数
#define MAX_SPACE 1000
//数组链表,用来存储序列
typedef struct {
int keys[MAX_NUM_OF_KEY];
int next;
}SLCell;
typedef struct {
SLCell R[MAX_SPACE];
int keynum;//关键字个数
int recnum;//待排序数据元素个数
}SLList;
typedef int ArrType[radix];
//分配
void Distribute(SLCell &R,int i,ArrType &f,Arrtype &r,int head) {
//静态链表的数据元素从数组下标为0处存放,next为-1代表是尾结点
for(j=0;j<radix;j++) f[j]=-1;
for(p=head;p!=-1;p=R[p].next) {
j=ord(R[p].keys[i]);//示意性操作,取R[p]的第i个关键字
if (f[j]==-1)f[j]=p;
else R[r[j]].next=p;
r[j]=p;
}
}
//收集
void collect(SLCell &R, int i, ArrType f, ArrType r, int &head) {
for(j=0; j<Radix &&f[j]==-1; j++);
head=f[j]; t=r[j];
while(j<Radix) {
for (++j; j<Radix-1 && f[j]==-1; j++);
if(f[j]!=-1) {
R[t].next=f[j];
t=r[j];
}
}
R[t].next=-1;
}
void RadixSort(SLList &L) {//建立静态链表,数据元素从数组下标为0处存放,
//next成员为-1代表是尾结点
//head存放链表的头指针
for(j=0; j<L.recnum-1; j++)
L.R[j].next=j+1;
L.R[L.recnum-1].next=-1;
head=0;
for(i=0; i<L.keynum ; i++) {
Distribute (L.R,i,f,r;head);
Collect(L.R,i,f,r,head);
}
}
总结
堆排序、快速排序、希尔排序、直接选择排序是不稳定的排序算法,而基数排序、冒泡排序、直接插入排序、折半插入排序、归并排序是稳定的排序算法