大部分内容基于中国大学MOOC的2021考研数据结构课程所做的笔记,该课属于付费课程(不过盗版网盘资源也不难找。。。)。后续又根据23年考研的大纲对内容做了一些调整,将二叉排序树和平衡二叉树的内容挪到了查找一章,并增加了并查集、平衡二叉树的删除、红黑树的内容。
排序一章的各种算法动态过程比较难以展现,所以阅读体验可能不是特别好。
西电的校内考试分机试和笔试。笔试占50分,机试2小时4道题占30分,做出2道满分,多做一道总分加5分。机试尽量把老师平时发的OJ题目都过一遍。笔试内容偏基础,但考的量比较大。
?
其他各章节的链接如下:
数据结构笔记(王道考研) 第一章:绪论
数据结构笔记(王道考研) 第二章:线性表
数据结构笔记(王道考研) 第三章:栈和队列
数据结构笔记(王道考研) 第四章:串
数据结构笔记(王道考研) 第五章:树和二叉树
数据结构笔记(王道考研) 第六章:图
数据结构笔记(王道考研) 第七章:查找
数据结构笔记(王道考研) 第八章:排序
其他各科笔记汇总
排序
排序的基本概念
什么是排序
排序(
S
o
r
t
Sort
Sort),就是重新排列表中的元素,使表中的元素满足按关键字有序的过程。
输入:
n
n
n个记录
R
1
,
R
2
,
.
.
,
R
n
R_1,R_2,..,R_n
R1?,R2?,..,Rn?,对应的关键字为
k
1
,
k
2
,
.
.
.
,
k
n
k_1,k_2,...,k_n
k1?,k2?,...,kn?。
输出:输入序列的一个重排
R
1
′
,
R
2
′
,
.
.
.
,
R
n
′
R_1^′,R_2^′,...,R_n^′
R1′?,R2′?,...,Rn′?,使得有
k
1
′
≤
k
2
′
≤
.
.
.
≤
k
n
′
k_1^′\le k_2^′\le...\le k_n^′
k1′?≤k2′?≤...≤kn′?(也可递减)
排序算法的评价指标
1.时间复杂度,空间复杂度
2.稳定性
若待排序表中有两个元素
R
i
R_i
Ri?和
R
j
R_j
Rj?,其对应的关键字相同即
k
e
y
i
=
k
e
y
j
key_i=key_j
keyi?=keyj?,且在排序前
R
i
R_i
Ri?在
R
j
R_j
Rj?的前面,若使用某一排序算法排序后,
R
i
R_i
Ri?仍然在
R
j
R_j
Rj?的前面,则称这个排序算法是稳定的,否则称排序算法是不稳定的。
排序的分类
排序算法可以分为
1.内部排序——数据都在内存中
2.外部排序——数据太多,无法全部放入内存
之所以有这种分类是因为磁盘的容量一般远大于内存,而运算速度又远不如内存。因此排序算法不仅要关注时间和空间复杂度,有时考虑到内存容量的问题还要关注如何使读/写磁盘次数更少
插入排序
每次将一个待排序的记录按其关键字大小插入到前面已排好序的子序列中,直到全部记录插入完成。
算法实现
void InsertSort(int A[],int n){
int i,j,temp;
for(i=1;i<n;i++)
if(A[i]<A[i-1]){
temp=A[i];
for(j=i-1;j>=0&&A[j]>temp;--j)
A[j+1]=A[j];
A[j+1]=temp;
}
}
算法实现(带哨兵)
void InsertSort(int A[],int n){
int i,j;
for(i=2;i<=n;i++)
if(A[i]<A[i-1]){
A[0]=A[i];
for(j=i-1;A[0]<A[j];--j)
A[j+1]=A[j];
A[j+1]=A[0];
}
}
优点:不用每轮循环都判断
j
≥
0
j\ge0
j≥0
算法效率分析
1.空间复杂度:
O
(
1
)
O(1)
O(1)
只需要定义几个所需空间为常数级的辅助变量(如
i
,
j
,
t
e
m
p
,
a
[
0
]
i,j,temp,a[0]
i,j,temp,a[0]),与问题的规模
n
n
n无关
?
2.时间复杂度:主要来自对比关键字,移动元素,若有
n
n
n个元素,则需要
n
?
1
n?1
n?1趟处理
下面的分析以带哨兵的版本为基础
?
最好情况:原本就有序,
O
(
n
)
O(n)
O(n)
共
n
?
1
n?1
n?1趟处理,每一趟只需要对比关键字1次,不用移动元素
?
最坏情况:原本为逆序,
O
(
n
2
)
O(n^2)
O(n2)
第一趟:对比关键字2次,移动元素3次
第二趟:对比关键字3次,移动元素4次
第
i
i
i趟:对比关键字
i
+
1
i+1
i+1次,移动元素
i
+
2
i+2
i+2次
…
第
n
?
1
n?1
n?1趟:对比关键字
n
n
n次,移动元素
n
+
1
n+1
n+1次
?
平均时间复杂度
O
(
n
2
)
O(n^2)
O(n2)
?
3.稳定性:稳定
优化 —— 折半插入排序
思路:先用折半查找找到应该插入的位置,再移动元素
当
l
o
w
>
h
i
g
h
low>high
low>high时折半查找停止,应将
[
l
o
w
,
i
?
1
]
[low,i?1]
[low,i?1]内的元素全部右移,并将
A
[
0
]
A[0]
A[0]复制到
l
o
w
low
low所指位置
如果
l
o
w
>
i
?
1
low>i?1
low>i?1,则什么都不做
当
A
[
m
i
d
]
=
=
A
[
0
]
A[mid]==A[0]
A[mid]==A[0]时,为了保证算法的“稳定性”,应继续在
m
i
d
mid
mid所指位置右边寻找插入位置
?
void InsertSort(int A[],int n){
int i,j,low,high,mid;
for(i=2;i<=n;i++){
A[0]=A[i];
low=1;high=i-1;
while(low<=high){
mid=(low+high)/2;
if(A[mid]>A[0]) high=mid-1;
else low=mid+1;
}
for(j=i-1;j>=high+1;--j)
A[j+1]=A[j];
A[high+1]=A[0];
}
}
比起“直接插入排序”,比较关键字的次数减少了,但是移动元素的次数没变,整体来看时间复杂度依然是
O
(
n
2
)
O(n^2)
O(n2)
对链表进行插入排序
移动元素的次数变少了,但是关键字对比的次数依然是
O
(
n
2
)
O(n^2)
O(n2)数量级,整体来看时间复杂度依然是
O
(
n
2
)
O(n^2)
O(n2)
希尔排序(Shell Sort)
先追求表中元素部分有序,再逐渐逼近全局有序
先将待排序表分割成若干形如
L
[
i
,
i
+
d
,
i
+
2
d
,
.
.
.
,
i
+
k
d
]
L[i,i+d,i+2d,...,i+kd]
L[i,i+d,i+2d,...,i+kd]的 “特殊” 子表,对各个子表分别进行直接插入排序。缩小增量
d
d
d,重复上述过程,直到
d
=
1
d=1
d=1为止
一般第一趟排序时设置增量
d
=
n
/
2
d=n/2
d=n/2,每一趟排序后将增量缩小一半
算法实现
void ShellSort(int A[],int n){
int d, i, j;
for(d= n/2; d>=1; d=d/2)
for(i=d+1; i<=n; ++i)
if(A[i]<A[i-d]){
A[0]=A[i];
for(j= i-d; j>0 && A[0]<A[j]; j-=d)
A[j+d]=A[j];
A[j+d]=A[0];
}
}
i
+
+
i++
i++会切换着处理每个子表
算法性能分析
1.空间复杂度:
O
(
1
)
O(1)
O(1)
?
2.时间复杂度:和增量序列
d
1
,
d
2
,
d
3
.
.
.
d_1,d_2,d_3...
d1?,d2?,d3?...的选择有关,目前无法用数学手段证明确切的时间复杂度。最坏时间复杂度为
O
(
n
2
)
O(n^2)
O(n2),当n在某个范围内时,可达
O
(
n
1.3
)
O(n^{1.3})
O(n1.3)
?
3.稳定性:不稳定
4.适用性:仅适用于顺序表,不适用于链表
冒泡排序
冒泡排序和快速排序是基于“交换”的排序
基于“交换”的排序:根据序列中两个元素关键字的比较结果来对换这两个记录在序列中的位置
?
从后往前(或从前往后)两两比较相邻元素的值,若为逆序(即
A
[
i
?
1
]
>
A
[
i
A[i?1]>A[i
A[i?1]>A[i]),则交换它们,直到序列比较完。称这样过程为“一趟”冒泡排序
第1趟排序使关键字值最小的1个元素“冒”到最前面
前边已经确定最终位置的元素不用再对比
第2趟结束后,最小的2个元素会“冒”到最前面
第3趟结束后,最小的3个元素会“冒”到最前面
…
若某一趟排序没有发生“交换”,说明此时已经整体有序
算法实现
void swap(int &a,int&b){
int temp=a;
a=b;
b=temp;
}
void BubbleSort(int A[],int n){
for(int i=0;i<n-1;i++){
bool flag=false;
for(int j=n-1;j>i;j--)
if(A[j-1]>A[j]){
swap(A[j-1],A[j]);
flag=true;
}
if(flag==false)
return;
}
}
算法性能分析
1.空间复杂度:
O
(
1
)
O(1)
O(1)
2.时间复杂度:
最好情况(有序)
比较次数
=
n
?
1
=n?1
=n?1;交换次数
=
0
=0
=0
最好时间复杂度
=
O
(
n
)
=O(n)
=O(n)
?
最坏情况(逆序)
比较次数
=
(
n
?
1
)
+
(
n
?
2
)
+
.
.
.
+
1
=
n
(
n
?
1
)
2
=
=(n?1)+(n?2)+...+1=n(n?1)2=
=(n?1)+(n?2)+...+1=n(n?1)2=交换次数
每次交换都需要移动元素3次
最坏时间复杂度
=
O
(
n
2
)
=O(n^2)
=O(n2)
?
平均时间复杂度
=
O
(
n
2
)
=O(n^2)
=O(n2)
?
3.稳定性:只有
A
[
j
?
1
]
>
A
[
j
]
A[j?1]>A[j]
A[j?1]>A[j]时才交换,因此算法是稳定的
是否适用于链表?
可从前往后“冒泡”,每一趟将更大的元素“冒’'到链尾
快速排序
算法思想:在待排序表
L
[
1...
n
]
L[1...n]
L[1...n]中任取一个元素
p
i
v
o
t
pivot
pivot作为枢轴(或基准,通常取首元素),通过一趟排序将待排序表划分为独立的两部分
L
[
1...
k
?
1
]
L[1...k?1]
L[1...k?1]和
L
[
k
+
1...
n
]
L[k+1...n]
L[k+1...n],使得
L
[
1..
k
?
1
]
L[1..k?1]
L[1..k?1]中的所有元素小于
p
i
v
o
t
pivot
pivot,
L
[
k
+
1...
n
]
L[k+1...n]
L[k+1...n]中的所有元素大于等于
p
i
v
o
t
pivot
pivot,则
p
i
v
o
t
pivot
pivot放在了其最终位置
L
(
k
)
L(k)
L(k)上,这个过程称为一次“划分”。然后分别递归地对两个子表重复上述过程,直至每部分内只有一个元素或为空为止,即所有元素放在了其最终位置上
算法实现
void QuickSort(int A[],int low,int high){
if(low<high){
int pivotpos=Partition(A,low,high);
QuickSort(A,low,pivotpos-1);
QuickSort(A,pivotpos+1,high);
}
}
int Partition(int A[],int low,int high){
int pivot=A[low];
while(low<high){
while(low<high&&A[high]>=pivot) --high;
A[low]=A[high];
while(low<high&&A[low]<=pivot) ++low;
A[high]=A[low];
}
A[low]=pivot;
return low;
}
算法效率分析
?
?
?
?
?
?
?
1.时间复杂度
=
O
(
n
×
=O(n\times
=O(n×递归层数
)
)
)
最好时间复杂度:
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2?n)
最坏时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
平均时间复杂度:
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2?n)
?
2.空间复杂度
=
O
(
=O(
=O(递归深度
)
)
)
最好时间复杂度:
O
(
l
o
g
2
n
)
O(log_2n)
O(log2?n)
最坏时间复杂度:
O
(
n
)
O(n)
O(n)
?
若每次选中的“枢轴”将待排序序列划分为均匀的两个部分,则递归深度最小,算法效率最高
若每次选中的“枢轴”将待排序序列划分为很不均匀的两个部分,则会导致递归深度增加,算法效率变低
若初始序列有序或逆序,则快速排序的性能最差(因为每次选择的都是最靠边的元素)
?
快速排序算法优化思路:尽量选择可以把数据中分的枢轴元素
eg:1.选头,中,尾三个位置的元素,取中间值作为枢轴元素;2.随机选一个元素作为枢轴元素
?
快速排序是所有内部排序算法中平均性能最优的排序算法
?
?
3.稳定性:不稳定
简单选择排序
简单选择排序和堆排序都属于选择排序
选择排序:每一趟在待排序元素中选取关键字最小(或最大)的元素加入有序子序列
?
n
n
n个元素的简单选择排序需要
n
?
1
n?1
n?1趟处理
最后剩一个不用再处理
算法实现
void swap(int &a,int&b){
int temp=a;
a=b;
b=temp;
}
void SelectSort(int A[],int n){
for(int i=0;i<n-1;i++){
int min=i;
for(int j=i+1;j<n;j++)
if(A[j]<A[min]) min=j;
if(min!=i) swap(A[i],A[min]);
}
}
算法性能分析
1.空间复杂度:
O
(
1
)
O(1)
O(1)
?
2.时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
无论有序、逆序、还是乱序,一定需要
n
?
1
n?1
n?1趟处理
总共需要对比关键字
(
n
?
1
)
+
(
n
?
2
)
+
.
.
.
+
1
=
n
(
n
?
1
)
2
(n?1)+(n?2)+...+1=\frac{n(n?1)}{2}
(n?1)+(n?2)+...+1=2n(n?1)?次
元素交换次数
<
n
?
1
<n?1
<n?1
?
3.稳定性:不稳定
4.适用性:既可以用于顺序表,也可以用于链表
堆排序
堆的定义
若
n
n
n个关键字序列
L
[
1..
n
]
L[1..n]
L[1..n]满足下面某一条性质,则称为堆(
H
e
a
p
Heap
Heap)
1.若满足:
L
(
i
)
≥
L
(
2
i
)
L(i)\ge L(2i)
L(i)≥L(2i)且
L
(
i
)
≥
L
(
2
i
+
1
)
(
1
≤
i
≤
n
/
2
)
L(i)\ge L(2i+1)(1\le i\le n/2)
L(i)≥L(2i+1)(1≤i≤n/2) ——大根堆(大顶堆)
2.若满足:
L
(
i
)
≤
L
(
2
i
)
L(i)\le L(2i)
L(i)≤L(2i)且
L
(
i
)
≤
L
(
2
i
+
1
)
(
1
≤
i
≤
n
/
2
)
L(i)\le L(2i+1)(1 \le i\le n/2)
L(i)≤L(2i+1)(1≤i≤n/2) ——小根堆(小顶堆)
回顾之前二叉树顺序存储的知识,大根堆在逻辑视角上可以看成所有子树根
≥
\ge
≥左、右的完全二叉树。堆排序就建立在堆顶元素关键字最大上
相应的小根堆也可以看成根
≤
\le
≤左,右的完全二叉树
建立大根堆
大根堆:根
≥
\ge
≥左,右
思路:把所有非终端结点都检查一遍,是否满足大根堆的要求,如果不满足,则进行调整
检查当前结点是否满足根
≥
\ge
≥左,右。若不满足,将当前结点与更大的一个孩子互换
若元素互换破坏了下一级的堆,则采用相同的方法继续往下调整(小元素不断“下坠”)
在顺序存储的完全二叉树中,非终端结点编号
i
≤
[
n
/
2
]
i\le[n/2]
i≤[n/2]
i
i
i的左孩子 ——
2
i
2i
2i
i
i
i的右孩子 ——
2
i
+
1
2i+1
2i+1
i
i
i的父节点 ——
[
i
/
2
]
[i/2]
[i/2]
?
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){
A[0]=A[k];
for(int i=2*k;i<=len;i*=2){
if(i<len&&A[i]<A[i+1])
i++;
if(A[0]>=A[i]) break;
else{
A[k]=A[i];
k=i;
}
}
A[k]=A[0]
}
基于大根堆进行排序
堆排序:每一趟将堆顶元素加入有序子序列(与待排序序列中的最后一个元素交换)
并将待排序元素序列再次调整为大根堆(小元素不断“下坠”)
注意:基于“大根堆”的堆排序得到“递增序列”,而基于“小根堆”的堆排序得到“递减序列”
?
代码如下
void BuildMaxHeap(int A[],int len)
void HeadAdjust(int A[],int k,int len)
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);
}
}
算法效率分析
堆排序的空间复杂度
=
O
(
1
)
=O(1)
=O(1)
?
?
下方有两个孩子,则“下坠”一层,需对比关键字2次。下方只有一个孩子,则下坠一层,只需对比关键字1次。故一个结点,每“下坠”一层,最多只需对比关键字2次
若树高为
h
h
h,某结点在第
i
i
i层,则将这个结点向下调整最多只需要“下坠”
h
?
i
h-i
h?i层,关键字对比次数不超过
2
(
h
?
i
)
2(h-i)
2(h?i),
n
n
n个结点的完全二叉树高
h
=
[
l
o
g
2
n
]
+
1
h=[log_2n]+1
h=[log2?n]+1
?
第
i
i
i层最多有
2
i
?
1
2^{i-1}
2i?1个结点,而只有第
1
~
(
h
?
1
)
1\sim (h-1)
1~(h?1)层的结点才有可能需要“下坠”调整
将整棵树调整为大根堆,关键字对比次数不超过
∑
i
=
h
?
1
1
2
i
?
1
2
(
h
?
i
)
=
∑
i
=
h
?
1
1
2
i
(
h
?
i
)
=
∑
j
=
1
h
?
1
2
h
?
j
j
≤
2
n
∑
j
=
1
h
?
1
j
2
j
≤
4
n
\sum_{i=h-1}^1 2^{i-1}2(h-i)=\sum_{i=h-1}^12^i(h-i)=\sum_{j=1}^{h-1}2^{h-j}j\le 2n\sum_{j=1}^{h-1}\frac{j}{2^j}\le 4n
∑i=h?11?2i?12(h?i)=∑i=h?11?2i(h?i)=∑j=1h?1?2h?jj≤2n∑j=1h?1?2jj?≤4n
∑
j
=
1
h
?
1
j
2
j
\sum_{j=1}^{h-1}\frac{j}{2^j}
∑j=1h?1?2jj?差比数列求和(错位相减法),求和结果小于2
?
建堆的过程,关键字对比次数不超过
4
n
4n
4n,建堆时间复杂度
=
O
(
n
)
=O(n)
=O(n)
?
?
初始建堆时间复杂度为
O
(
n
)
O(n)
O(n)
每趟交换和建堆过程,根节点最多“下坠”
h
?
1
h-1
h?1层,每下坠一层最多只需对比关键字2次,因此每一趟排序时间复杂度不超过
O
(
h
)
=
O
(
l
o
g
2
n
)
O(h)=O(log_2n)
O(h)=O(log2?n)。共
n
?
1
n-1
n?1趟,总的时间复杂度
=
O
(
l
o
g
2
n
)
=O(log_2n)
=O(log2?n)
堆排序的时间复杂度
=
O
(
n
)
+
O
(
n
l
o
g
2
n
)
=
O
(
n
l
o
g
2
n
)
=O(n)+O(nlog_2n)=O(nlog_2n)
=O(n)+O(nlog2?n)=O(nlog2?n)
?
?
堆排序是不稳定的
堆的插入删除
在堆中插入新元素
对于小根堆,新元素放到表尾,与父节点对比,若新元素比父节点更小,则将二者互换。新元素就这样一路“上升”,直到无法继续上升为止
在堆中删除元素
被删除的元素用堆顶元素替代,然后让该元素不断“下坠”,直到无法下坠为止
归并排序(Merge Sort)
?
Merge(归并/合并)
归并:把两个或多个已经有序的序列合并成一个
“2路”归并 —— 每选出一个小元素就需对比关键字1次。“4路”归并 —— 每选出一个小元素就需对比关键字3次。故
m
m
m路归并,每选出一个元素需要对比关键字
m
?
1
m-1
m?1次
归并排序(手算模拟)
在内部排序中一般采用2路归并
核心操作:把数组内的两个有序序列归并为一个
代码实现
int *B=(int *)malloc(n*sizeof(int));
void Merge(int A[],int low,int mid,int high){
int i,j,k;
for(k=low,k<=high;k++)
B[k]=A[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(int 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);
}
}
算法效率分析
2路归并的“归并树”——形态上就是一棵倒立的二叉树
二叉树的第
h
h
h层最多有
2
h
?
1
2h?1
2h?1个结点
若树高为h,则应满足
n
≤
2
h
?
1
n\le2h?1
n≤2h?1
即
h
?
1
=
[
l
o
g
2
n
]
h?1=[log2n]
h?1=[log2n]
结论:
n
n
n个元素进行2路归并排序,归并趟数
=
[
l
o
g
2
n
]
=[log_2n]
=[log2?n]
每趟归并时间复杂度为
O
(
n
)
O(n)
O(n),则算法时间复杂度为
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2?n)
?
空间复杂度
=
O
(
n
)
=O(n)
=O(n),来自于辅助数组B
?
两个元素相等时,优先使用靠前的那个。归并排序是稳定的
基数排序(Radix Sort)
基数排序
?
?
?
?
?
?
?
?
?
?
?
假设长度为
n
n
n的线性表中每个结点
a
j
a_j
aj?的关键字由
d
d
d元组(
k
j
d
?
1
,
k
j
d
?
2
,
.
.
.
,
k
j
1
,
k
j
0
k_j^{d?1},k_j^{d?2},...,k_j^1,k_j^0
kjd?1?,kjd?2?,...,kj1?,kj0?)组成。其中,
0
≤
k
j
i
≤
r
?
1
(
0
≤
j
≤
n
,
0
≤
i
≤
d
?
1
)
0\le k_j^i\le r?1(0\le j\le n,0\le i\le d?1)
0≤kji?≤r?1(0≤j≤n,0≤i≤d?1),
r
r
r称为“基数”
k
j
d
?
1
k_j^{d?1}
kjd?1?为最高位关键字(最主位关键字),
k
j
0
k_j^0
kj0?为最低位关键字(最次位关键字)
?
基数排序得到递减序列的过程如下,
初始化:设置
r
r
r个空队列,
Q
r
?
1
,
Q
r
?
2
,
.
.
.
,
Q
0
Q_{r?1},Q_{r?2},...,Q_0
Qr?1?,Qr?2?,...,Q0?
按照各个关键字位权重递增的次序(个、十、百),对
d
d
d个关键字位分别做“分配”和“收集”
分配:顺序扫描各个元素,若当前处理的关键字位
=
x
=x
=x,则将元素插入
Q
x
Q_x
Qx?队尾
收集:把
Q
r
?
1
,
Q
r
?
2
,
.
.
.
,
Q
0
Q_{r?1},Q_{r?2},...,Q_0
Qr?1?,Qr?2?,...,Q0?各个队列中的结点依次出队并链接
可见基数排序不是基于“比较”的排序算法
?
基数排序得到递增序列的过程如下,
初始化:设置
r
r
r个空队列,
Q
0
,
Q
1
,
.
.
.
,
Q
r
?
1
Q_{0},Q_{1},...,Q_{r-1}
Q0?,Q1?,...,Qr?1?
按照各个关键字位权重递增的次序(个、十、百),对
d
d
d个关键字位分别做“分配”和“收集”
分配:顺序扫描各个元素,若当前处理的关键字位
=
x
=x
=x,则将元素插入
Q
x
Q_x
Qx?队尾
收集:把
Q
0
,
Q
1
,
.
.
.
,
Q
r
?
1
Q_{0},Q_{1},...,Q_{r-1}
Q0?,Q1?,...,Qr?1?各个队列中的结点依次出队并链接
算法效率分析
基数排序通常基于链式存储实现
typedef struct LinkNode{
ElemType data;
struct LinkNode *next;
}LinkNode, *LinkList;
typedef struct{
LinkNode *front,*rear;
}LinkQueue;
一般不太考察基数排序的代码实现
?
1.空间复杂度
需要
r
r
r个辅助队列,空间复杂度
O
(
r
)
O(r)
O(r)
?
2.时间复杂度:
把关键字拆为
d
d
d个部分,每个部分可能取得
r
r
r个值
一趟分配
O
(
n
)
O(n)
O(n),一趟收集
O
(
r
)
O(r)
O(r),总共
d
d
d趟分配、收集,总的时间复杂度
=
O
(
d
(
n
+
r
)
)
=O(d(n+r))
=O(d(n+r))
收集一个队列只需
O
(
1
)
O(1)
O(1)时间
p->next = Q[6].front;
Q[6].front = NULL;
Q[6].rear = NULL;
?
3.稳定性:稳定的
基数排序的应用
外部排序
内存、外存之间的数据交换
外部排序原理
构建初始”归并段“
归并过程
第一趟归并:把8个有序子序列(初始归并段)两两归并
?
第二趟归并:把4个有序子序列(归并段)两两归并
归并之后得到的更长子序列放在磁盘的另一片空间当中,以前的这两片空间会归还给系统,这里只是为了美观
?
第三趟归并:将2个有序子序列(归并段)归并
经过3趟归并,整体有序
时间开销分析
优化
1.多路归并
[
l
o
g
k
r
]
[log_kr]
[logk?r]向上取整
2.减少初始归并段数量
什么是多路平衡归并?
[
m
/
k
]
[m/k]
[m/k]向上取整
败者树
败者树的构造
败者树的使用
败者树在多路平衡归并中的应用
[
l
o
g
2
k
]
[log_2k]
[log2?k]向上取整
败者树的实现思路
置换-选择排序
?
?
上面的演示每次读写一个记录,忽略了输出缓冲区和输入缓冲区
最佳归并树
归并树的性质
构造2路归并的最佳归并树
多路归并的情况
?
添加虚段的数量
|