数据结构复习笔记
第八章排序
排序的基本概念
- 排序:就是重新排列表中的元素,使得表中的元素满足按照关键字有序的过程;
- 算法的稳定性:关键字相同的两个元素,若排序前和排序后的顺序都一样/排序后相对位置不变,则算法稳定。⚠注意算法稳定不是衡量算法优劣的标准。
- 内外部排序的区分:数据元素是否完全在内存内;内部排序算法的性能取决于算法的时间复杂度(由比较和移动次数决定)和空间复杂度。
插入排序
基本思想:每次将一个待排序的记录按照关键字大小插入前面已经拍好的子序列中,直到全部记录插入完成
- 直接插入排序
void InsertSort(ElemType A[],int n){
int i,j;
for(i=2;i<=n;i++){ //依次将2-n个数字插入到前面的排序数列中
if(A[i]<A[i-1]){ //如果待排序数字小于前驱,则插入有序表中
A[0]=A[i]; //复制为哨兵
for(j=i-1;j>=0&&A[0]<A[j];--j){ //从前往后查找待插入的位置
A[j+1]=A[j];
}
A[j+1]=A[0]; //因为上面循环中是--j,所以在做完所有后移跳出循环额时候,j向前进了1
}
}
}
比较次数和移动次数取决于待排序表的初始状态!
稳定的排序算法,且适用于顺序存储方式和链式存储的线性表;
空间效率:常数个辅助单元,空间复杂度为O(1);
时间复杂度:O(n^2);
- 折半插入排序
void InsertSort(ElemType A[], int n) {
int i,j, mid, low, high;
for (i = 2;i <= n;i++) {
A[0] = A[i];
low = 1;
high = i - 1;
while (low <= high) {
mid = (low + high) / 2;
if (A[0] > A[mid]) {
low = mid + 1;
}
else
high = mid - 1;
}
for (j = i - 1;j >= high + 1;--j) { //统一后移,空出插入位置
A[j + 1] = A[j];
}
A[high + 1] = A[0];
}
}
比较次数(O(nlogn))与待排序的初始状态无关,仅取决于表中的元素个数n;
移动次数未改变,依赖于表中的初始状态,因此时间复杂度为O(n^2);
算法稳定,仅适用于顺序存储的顺序表;
对于数据量不大的排序表,折半插入有很好的性能
- 希尔排序
void SellInsort(int *a, int n) {
int dk,i,j; //定义增量;
for (dk = n / 2;dk >= 1;dk = dk / 2) {
for (i = dk + 1;i <= n;i++) {
if (a[i] < a[i - dk]) {
a[0] = a[i];
for ( j = i - dk;j > 0 && a[0] < a[j];j -= dk) {
a[j + dk] = a[j];
}
a[j + dk] = a[0];
}
}
}
}
时间复杂度:依赖于增量序列的函数,约为O(n^1/3),最坏为O(n*n);
不稳定,且仅适用于顺序存储的线性表;
🐾***注意点***
简单选择排序、冒泡排序、堆排序、快速排序依次排序后,会使一个记录放在最终位置上。
有问题的题目
//记住公式就行
对任意7个关键字进行基于比较的排序,至少要进行()次关键字之间的两两比较?
对任意n个关键字排序的比较次数至少为log(n!)。(取上整)。
交换排序
根据序列中两个元素关键字的比较结果来对换两个记录在序列中的位置
- 冒泡排序
从后往前两两比较相邻元素的值,若为逆序则交换
void BubbleSort(int* a, int n) {
for (int i = 0;i < n;i++) {
int flag = false; //表示本趟冒泡排序是否发生交换的标志
for (int j = n - 1;j > i;j--) { //一趟排序的过程
if (a[j] < a[j - 1]) { //若为逆序
swap(a[j - 1], a[j]); //交换
flag = true;
}
}
if (flag == false) { //本次遍历之后没有发生交换,则说明表已经有序
return;
}
}
}
空间效率:只需要常数个辅助单元;
时间效率:最好的情况(初始序列有序),比较次数为n-1,移动次数为0,O(0);
最坏的情况(初始序列逆序),比较次数n(n-1)/2,移动次数3n(n-1)/2;
平均时间复杂度O(n^2);
稳定,且每一趟排序都会有一个元素放置到最终的位置上。
- 快速排序
每次总以当前表中第一个元素作为枢纽来对表进行划分,将比枢纽大的元素右移,比枢纽小的左移;
int Partition(ElemType A[], int low, int high) {
ElemType provit = A[low]; //将表中第一个元素设置为枢纽
while (low < high) {
while (low < high && A[high] >= provit)
--high;
A[low] = A[high];
while (low < high && A[low] <= provit)
++low;
A[high] = A[low];
}
A[low] = provit;
return low;
}
void QuickSort(ElemType A[], int low, int high) {
if (low < high) {
int provitpos = Partition(A, low, high);
QuickSort(A, low, provitpos - 1);
QucikSort(A, provitpos + 1, high);
}
}
空间效率:快排是递归的,需要一个递归站来保存每层递归调用的必要信息,其容量与递归调用的最大深度一致,最好O(logn),最坏O(n),平均O(logn);
时间效率:快排与划分是否对称有关,最坏O(n*2),最好O(nlogn);
快速排序是所有内部排序算法中平均性能最优的排序算法,不稳定,每趟排序后会将枢纽元素放在其最终位置上。宜采用顺序存储
---------------------------------------------------------练习------------------------------------------------------------
编写双向冒泡排序,在正反两个方向交替进行扫描,即第一趟把关键字最大的元素放在序列的最后面,第二趟把关键字最小的元素放在序列的最前面,反复操作
//我自己写的
void BubbleSort(ElemType A[], int n) {
int flag1 = false;
for (int i = 1;i <= n/2;i++) {
if (flag1 == false) {
for (int j = n;j > i;j--) {//单向冒泡排序的时候,一段是固定的,所以可以用n
//但是这里是双向冒泡排序,所以两端都不固定,不能一直用n/1
if (A[j - 1] > A[j]) {
swap(A[j - 1], A[j]);
flag1 = true;
}
if (flag1 == true)
break;
}
}
if (flag1 == true) {
for (int j = 1;j <n-i;j++) {
if (A[j +1] < A[j]) {
swap(A[j + 1], A[j]);
flag1 = false;
}
if (flag1 == false)
break;
}
}
}
}
//王道的答案
void BubbleSort(ElemType A[], int n) {
int low = 0, high = n - 1;
bool flag = true;
while (low <= high && flag) {
flag = false;
for (int i = low;i < high;i++) {
if (A[i] > A[i + 1]) {
swap(A[i], A[i + 1]);
flag = true;
}
}
high--;
for (int i = high;i > low;i--) {
if (A[i] > A[i - 1]) {
swap(A[i - 1], A[i]);
flag = true;
}
}
low++;
}
}
已知线性表按照顺序存储,且每个元素都是不相同的整数型元素。设计把所有奇数移动到所有偶数的前面的算法。要求时间最少辅助空间最少。(快速排序!!)
//采用快速排序思想,先从后往前找到一个偶数,再从后往前找到一个奇数,两个位置调换
void move(ElemType A[], int low, int high) {
while (low < high) {
while (low < high && A[low] % 2 != 0)
low++;
while (low < high && A[high] % 2 == 0)
high--;
if (low < high)
swap(A[low], A[high]);
low++;
high--;
}
}
试编写一个算法,使之能够在数组L[1…n]中找出第k小的元素,即从小到大排序后处于第k个位置的元素
//这可以用冒泡排序算法
int BubbleSort(ElemType A[], int n, int k) {
int i;
for(i=1;i <= k;i++) {
for (int j = n;j > i;j--) {
if (A[j] < A[j - 1]) {
swap(A[j], A[j - 1]);
}
}
}
return A[k];
}
//王道书上用的是快排,然后讨论枢纽点和k的位置,进行递归
int Partition(ElemType A[], int low, int high) {
int privot = A[low];
while (low < high) {
while (low<high && A[high]>privot)
high--;
A[low] = A[high];
while (low < high && A[low] < privot)
low++;
A[high] = A[low];
}
A[low] = privot;
return low;
}
int Qucik(ElemType A[], int m,int low,int high) {
if (low < high) {
int privot = Partition(A, low, high);
if (privot = m)
return privot;
if (privot > m)
Qucik(A, m, low, privot - 1);
if (privot < m)
Qucik(A,m, privot + 1, high);
}
}
2016年408真题 王道书P307页
思路:仿照快速排序的思想,基于枢纽将n个整数划分为两个子集,然后根据划分后枢纽所处的位置i分别处理;
第一种情况:i=n/2,则算法结束;
第二种情况:i<n/2,对i之后的元素继续进行划分,直到变成第一种情况;
第三种情况:i>n/2,对i之前的元素继续进行划分,直到变成第一种情况;
//2016年真题 利用快速排序进行讨论枢纽和n/2的位置
int Partition2(int A[], int low, int high) {
int privot = A[low];
while (low < high) {
while (low<high && A[high]>=privot)
--high;
A[low] = A[high];
while (low < high && A[low]<=privot)
++low;
A[high] = A[low];
}
A[low] = privot;
return low;
}
void Quick(int A[],int n,int low,int high) {
int low = 1;
int high = n;
int flag = 1;
while (flag) {
int privote = Partition2(A, low, high);
if (privote == n / 2)
flag = 0; //划分成功
if (privote < n / 2) {
Qucik(A, n, privote + 1, high);
}
else
Qucik(A, n, low, privote - 1);
}
}
//书上的答案
int sort(int a[], int n) {
int pri, low = 0, low0 = 0, high = n - 1, high0 = n - 1,
flag = 1, k = n / 2, i;
int s1 = 0, s2 = 0;
while (flag) {
pri = a[low];
while (low < high) {
while (low < high && a[high] >= pri)
--high;
a[low] = a[high];
while (low < high && a[low] <= pri)
++low;
a[high] = a[low];
}
a[low] = pri;
if (low == k - 1)
flag = 0;
else {
if (low < k - 1) {
low0 = ++low;
high = high0;
}
else {
high0 = --high;
low = low0;
}
}
}
for (i = 0;i < k;i++)
s1 += a[i];
for (i = k;i < n;i++) {
s2 += a[i];
}
return s2 - s1;
}
荷兰国旗问题
解题思路:顺序扫描线性表,将红色条块交换到线性表的最前面,蓝色的到最后面,为此设置三个指针。其中,j为工作指针,表示当前扫描元素的颜色,决定将其交换到序列的前面或者后面。
//荷兰国旗问题
//较为特殊,是三种元素排序,且要求时间为n
//设置枚举数组
typedef enum {
RED, WHITE, BLUE
}color;
void Flag_Arrange(color a[], int n) {
//i表示i前面所有的元素全为红,k表示k后面的元素全为蓝
int i = 0, j = 0, k = n - 1; //j表示当前工作指针
while (j < k) {
switch (a[j]) {
case RED:swap(a[i], a[j]);
i++;
j++;
break;
case WHITE:j++;
break;
case BLUE:swap(a[j], a[k]);
k--;
}
}
}
选择排序
- 简单选择排序
第i趟排序从待排序列表中选取关键字修小的元素与L[i]交换,每趟排序可以确定一个元素的最终位置,经过n-1趟排序就能使得整个排序表有序
//简单选择排序
void SelectSort(ElemType A[], int n) {
for (int i = 1;i <= n - 1;i++) { //一共进行n-1趟
int min = i; //记录最小元素的位置
for (int j = i + 1;j < n;i++) {
if (A[j] > A[min]) {
min = j;
}
}
if (min != i) {
swap(A[i], A[min]);
}
}
空间效率:仅有常数个辅助单元,所以空间效率为O(1);
时间效率:移动次数很少,不会超过3(n-1),但是比较次数与序列的初始排序状态无关,始终是n(n-1)/2;
不稳定排序!!!
- 堆排序
满足:L[i]>=L[2i]且L[i]<=L[2i+1]——大根堆
L[i]<=L[2i]且L[i]<=L[2i+1]——小根堆
//堆排序
//建立大根堆的算法
void BuildMaxHeap(int* a, int len) {
for (int i = len / 2;i > 0;i--)//从后往前处理(最下面的子树开始处理)
HeadAdjust(a, i, len);
}
void HeadAdjust(int* a, int k, int len) {
//将元素k为根的子树进行调整
a[0] = a[k]; //暂存子树的根结点
for (int i = 2 * k;i < len;i *= 2) {
if (i < len && a[i] < a[i + 1]) //判断根结点的左右孩子谁更大
i++; //取key较大的子节点的下标
if (a[0] >= a[i]) //符合大根堆要求,筛选结束
break;
else {
a[k] = a[i]; //将a[i]调整到双亲节点上
k = i; //修改k值,以便继续下坠
}
}
a[k] = a[0];
}
//堆排序算法
void HeapSort(int* a, int len) {
BuildMaxHeap(a, len);
for (int i = len;i > 1;i--) {
swap(a[i], a[1]);
HeadAdjust(a, 1, i - 1);
}
}
----------------------------------------王道书练习---------------------------------------
堆和二叉排序树的区别?
以小根堆为例。堆的特点是双亲结点的关键字必然小于等于该孩子结点的关键字,而两个孩子结点没有顺序要求。而二叉排序树中,每个双亲结点的关键字大于左子树结点的关键字,,小于右子树结点的关键字,每个结点的两个孩子值有次序关系。对二叉排序树进行中序遍历,会得到一个有序的序列,而堆不一定能够得到一个有序的序列。
编写一个算法,判断一个数据序列是否构成一个小根堆
🐖:如果不分成奇偶数情况讨论,那么在偶数情况下,程序会自动补齐最后一个右孩子,数组溢出
//设计一个算法,判断一个数据序列是否构成一个小根堆
void smallgrain(ElemType A[], int len) {
if (len % 2 == 0) { //若len为偶数,则有一个单分支结点
if (A[len / 2] > A[len])//判断单分支结点
return false;
for (int i = len / 2-1;i >= 1;i--) {//判断除了单分支结点以外的所有双分支结点
if (A[i] > A[2 * i] || A[i] > A[2 * i + 1]) {
return false;
}
}
}
else { //len为奇数,所有的分支均为双分支结点
for (int i = len / 2;i >= 1;i--) {
if (A[i] > A[2 * i] || A[i] > A[2 * i + 1]) {
return false;
}
}
}
return true;
}
编写一个算法,在基于单链表表示的待排序关键字序列上进行简单选择排序
//每趟在原始链表中摘下关键字最大的结点,把它插入结果链表的最前端;
//由于在原始链表中摘下的关键字越来越小,在结果链表前端插入的关键字也变得越来越小,因此最后形成的结果链终止的结点将按照关键字非递减的顺序有序链接
void BreifSort(LinkListed& L) {
LNode* h = L, * p, * r, * s;
L = NULL;
while (h != NULL) {
p = s = h;
q = r = NULL;
while (p != NULL) {
//s和r记忆最大结点和其前驱,p为工作指针,q为其前驱
if (p->data > s->data) {
s = p;
r = q;
}
q = p;
p = p->link;
}
if (s == h) //最大结点在原链表的前端
h = h->link;
else
r->link = s->link; //最大结点在原链表内
s->link = L; //结点s插入结果链表前端
L = s;
}
}
归并排序和基数排序
- 归并排序
归并的含义:将两个或者两个以上的有序表组合成一个新的有序表
//归并排序
//设两段有序表存放在同一顺序表中的相邻位置,先将他们复制到辅助数组B,每次从b中的两端取出一个记录
//进行关键字比较,较小的放入A,当数组b中有一段的下标超出其对应的表长时,将另一段中剩余的部分直接复制到A
ElemType* B = (ElemType *) malloc ((n + 1) * sizeof(ElemType));
void Merge(ElemType A[], int low, int mid, int high) {
//表A中的两端各自有序,将他们合并成一个有序表
for (int k = low;k <= high;k++) {
B[k] = A[k]; //将A中所有元素复制到B中
}
int i, j, k;
for ( i = low, j = mid + 1, k = i;i <= mid && j <= high;k++) {
if (B[i] < B[j]) {
A[k] = B[i];
}
else
A[k] = B[j];
}
while (i <= mid) {
A[k++] = B[i++];
}
while (j <= high)
A[k++] = B[j++];
}
//二路归并
void MergeSort(ElemType A[], int low, int high) {
if (low < high) {
int mid = (low + high) / 2;
MergeSort(A, low, mid);
MergeSort(A, mid + 1, high);
Merge(A, low, mid, high);
}
}
空间效率:Merge中需要n个辅助单元,则空间效率为O(n);
时间效率:每趟归并的时间复杂度为O(n),共需要进行logn趟归并,所以时间复杂度为O(nlogn);
稳定性:算法是稳定的
- 基数排序
一种页数的排序方法,不基于比较和移动进行排序,而是基于关键字各位的大小进行排序≠关键字大小
为实现多关键字排序,第一种是最高位优先(MSD)法,按照关键字位权重递减依次逐层划分成若干更小的子序列;第二种最低位优先(LSD)法。
空间效率:一趟排序需要的辅助空间为r(r个队列:r个对头指针和r个队尾指针),空间复杂度Q®;
时间效率:基数排序需要进行d趟分配和收集,一趟分配需要O(n),一趟收集O®,所以时间复杂度O(d(n+r)),他与序列的初始状态无关
稳定性:稳定;
---------------------------------------王道书练习题-------------------------------
//设顺序表用数组A表示,表中的元素存储在数组下标1-m+n的范围内,前m个元素递增有序,后n个元素递增有序
//设计一个算法使得整个顺序表有序
/************算法思想:将A分为前后两个部分,然后进行直接插入比较*********************/
void Sort1(int* A, int m, int n) {
for (int i = m + 1;i <= m + n;i++) { //对m+1到最后进行直接插入排序
if (A[i] < A[i-1]){
A[0] = A[i]; //将插入的A[i]的值复制到A[0]中去,防止消失
for (int k = i - 1;k >=0&&A[k]>A[0];k--) {//将A[i]-A[j]的所有数后移一位
A[k + 1] = A[k];
}
A[j+1] = A[0];
}
}
}
//王道书答案
void Sort2(int* A, int m, int n) {
int i, j;
for (i = m + 1;i <= m + n;i++) {
A[0] = A[i];
for (j = i - 1;A[j] > A[0];j--)
A[j + 1] = A[j];
A[j + 1] = A[0];
}
}
//计数排序
//对待排序的表进行排序,并将排序结果存放到另一个新表中
//表中待排序的关键码互不相同
//对表中每一个记录,扫描待排序的表一遍,统计待排序的表中有多少个记录的关键码比该记录的关键码小
//假设针对某个记录统计出的计数值为C,则这个记录在新的有序表中合适的存放位置即为C
void Sort3(int* A, int n) {
int* B; //设置一个和A相同长度的数组B,存储新排序的数组
int k = 1; //计数变量
for (int i = 1;i <= n;i++) {
for (int j = 1;j <= n;j++) {
/*******************
if (A[j] == A[i]) //表中所有待排序的关键码不相同,若相同则删除
delete A[i];
*********************/
if (A[j] < A[i])
k++;
}
B[k] = A[i];
k = 0;
}
}
//关键码的比较次数是n*n次
//设有一个数组中存放了一个无序的关键数列K1,K2,K3....Kn,现要求将Kn放在将元素排序后的正确位置上
//试编写实现该功能的算法,要求比较关键字的次数不超过n
/*****算法思想:进行快速排序,将第一次枢纽选择变成最后一个,先从前往后,再从后往前**********/
int Sort4(int* A, int low, int high) {
int prit = A[high];
while (low < high) {
while (low < high && A[low] <= prit)
low++;
A[high] = A[low];
while (low < high && A[high] >= prit)
high--;
A[high] = A[low];
}
A[high] = prit;
return high;
}
void QuickSort4(int* A, int low, int high) {
if (low < high) {
int prit = Sort4(A, low, high);
QuickSort4(A, low, prit - 1);
QuickSort4(A, prit + 1, high);
}
}
外部排序
外部排序值得是待排序文件较大,内存依次放不下,需要存放在外存的文件的排序;为了减少平衡归并中外存读写次数所采取的方法:增大归并路数和减少归并段个数,利用败者树增大归并路数,利用置换-选择排序增大归并段长度来减少归并段个数。
将待排序的记录存储在外存上,排序时再把数据一部分一部分的调入内存进行排序,在排序过程中需要多次进行内存和外存之间的交换。因为读取磁盘的时间远远大于内存中操作的时间,所以时间复杂度一般看磁盘读取时间,即I/O次数。
-
多路平衡归并树和败者树
为了使得内部归并不受k(平衡数)增大的影响,引入了败者树,可以视为一个完全二叉树;
使用败者树之后内部归并的比较次数与k无关,但是k并不是越大越好,因为k越大需要增加输入缓冲区的个数 -
置换-选择排序(生成初始归并段)
探索新的方法用来产生更长的初始归并段 -
最佳归并树
推广情形下的哈夫曼树