mirror of
https://github.com/Didnelpsun/CS408.git
synced 2026-06-16 15:07:38 +08:00
更新排序
This commit is contained in:
@@ -156,6 +156,7 @@
|
||||
<ClInclude Include="search.h" />
|
||||
<ClInclude Include="sequence_list.h" />
|
||||
<ClInclude Include="sequence_string.h" />
|
||||
<ClInclude Include="sort.h" />
|
||||
<ClInclude Include="static_link_list.h" />
|
||||
<ClInclude Include="sequence_queue.h" />
|
||||
</ItemGroup>
|
||||
|
||||
180
Code/sort.h
180
Code/sort.h
@@ -76,4 +76,184 @@ int ShellSort(element_type data[], int length) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 冒泡排序
|
||||
int BubbleSort(element_type data[], int length) {
|
||||
for (int i = 0; i < length - 1; i++) {
|
||||
// 设置一个标志表示本趟冒泡排序是否发生交换
|
||||
int flag = 0;
|
||||
element_type temp;
|
||||
for (int j = length - 1; j > i; j--) {
|
||||
if (data[j - 1] > data[j]) {
|
||||
temp = data[j - 1];
|
||||
data[j - 1] = data[j];
|
||||
data[j] = temp;
|
||||
flag = 1;
|
||||
}
|
||||
}
|
||||
// 如果本次遍历后没有发生交换,就代表已经有序
|
||||
if (flag == 0) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 快速排序划分
|
||||
int QuickPart(element_type data[], int low, int high) {
|
||||
// 选择data[low]作为基准
|
||||
int pivot = data[low];
|
||||
// 用low和high搜索基准的最终位置
|
||||
while (low < high) {
|
||||
// 如果当前high指向的元素大于基准就移动high指针
|
||||
while (low < high && data[high] >= pivot) {
|
||||
--high;
|
||||
}
|
||||
// 比标准小的元素移动到low指向的元素
|
||||
data[low] = data[high];
|
||||
// 交换移动指针
|
||||
// 如果当前low指向的元素小于基准就移动low指针
|
||||
while (low < high && data[low] <= pivot) {
|
||||
++low;
|
||||
}
|
||||
// 比标准大的元素移动到high指向的元素
|
||||
data[high] = data[low];
|
||||
}
|
||||
// 当low和high指向同一个元素时就把基准放到这个位置
|
||||
data[low] = pivot;
|
||||
// 返回存放基准元素的位置
|
||||
return low;
|
||||
}
|
||||
|
||||
// 快速排序
|
||||
int QuickSort(element_type data[], int low, int high) {
|
||||
// 递归跳出条件,即low=high,表中只有一个元素
|
||||
if (low < high) {
|
||||
// 进行划分
|
||||
int pivot = QuickPart(data, low, high);
|
||||
// 对划分的左子表进行处理
|
||||
QuickSort(data, low, pivot - 1);
|
||||
// 对划分的右子表进行处理
|
||||
QuickSort(data, pivot + 1, high);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 简单选择排序
|
||||
int SimpleSelectSort(element_type data[], int length) {
|
||||
element_type temp;
|
||||
// 一共进行n-1趟
|
||||
for (int i = 0; i < length - 1; i++) {
|
||||
// 记录最小元素的位置
|
||||
int min = i;
|
||||
// 在data[i,length-1]中选择最小的元素
|
||||
for (int j = i + 1; j < length; j++) {
|
||||
// 更新最小元素位置
|
||||
if (data[j] < data[min]) {
|
||||
min = j;
|
||||
}
|
||||
}
|
||||
// 如果当前最小元素的值不等于当前指向位置就交换
|
||||
if (min != i) {
|
||||
temp = i;
|
||||
i = min;
|
||||
min = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 建立大根堆
|
||||
int BuildMaxHeap(element_type data[], int length) {
|
||||
for (int i = length / 2; i > 0; i--) {
|
||||
MaxHeadAdjust(data, i, length);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 以node为根的子树调整为大根堆
|
||||
int MaxHeadAdjust(element_type data[], int node, int length) {
|
||||
// 使用data[0]暂存子树根结点
|
||||
data[0] = data[node];
|
||||
// 沿key较大的子结点向下筛选
|
||||
for (int i = 2 * node; i <= length; i *= 2) {
|
||||
// 取key较大的子节点的下标
|
||||
if (i < length && data[i] < data[i + 1]) {
|
||||
i++;
|
||||
}
|
||||
// 如果根大于左右子结点则代表不用调整
|
||||
if (data[0] >= data[i]) {
|
||||
break;
|
||||
}
|
||||
else {
|
||||
// 将data[i]放到父结点上
|
||||
data[node] = data[i];
|
||||
// 修改node值,以继续向下筛选
|
||||
node = i;
|
||||
}
|
||||
}
|
||||
// 被筛选结点的值最后放到最后的位置
|
||||
data[node] = data[0];
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 大根堆的堆排序
|
||||
int MaxHeapSort(element_type data[], int length) {
|
||||
// 初始化建立一个大根堆
|
||||
BuildMaxHeap(data, length);
|
||||
// 建立一个交换变量
|
||||
element_type temp;
|
||||
// n-1趟交换和建立的过程
|
||||
for (int i = length; i > 1; i--) {
|
||||
// 堆顶元素与堆底元素互换
|
||||
temp = data[i];
|
||||
data[i] = data[1];
|
||||
data[1] = temp;
|
||||
// 把剩余的待排序元素调整为堆
|
||||
MaxHeadAdjust(data, i, i - 1);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 归并排序辅助数组
|
||||
element_type* aid = (element_type*)malloc(MAXSIZE * sizeof(element_type));
|
||||
|
||||
// data[low,mid]和data[mid+1,high]各自有序,将两个部分归并
|
||||
int Merge(element_type data[], int low, int mid, int high) {
|
||||
int i, j, k;
|
||||
for (k - low; k <= high; k++) {
|
||||
// 将data中所有元素复制到辅助数组中
|
||||
aid[k] = data[k];
|
||||
}
|
||||
for (i = low, j = mid + 1, k = i; i <= mid && j <= high; k++) {
|
||||
if (aid[i] <= aid[j]) {
|
||||
data[k] = aid[i++];
|
||||
}
|
||||
else {
|
||||
data[k] = aid[j++];
|
||||
}
|
||||
}
|
||||
while (i <= mid) {
|
||||
data[k++] = aid[i++];
|
||||
}
|
||||
while (j <= high) {
|
||||
data[k++] = aid[j++];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 归并排序
|
||||
int MergeSort(element_type data[], int low, int high) {
|
||||
if (low < high) {
|
||||
// 从中间划分
|
||||
int mid = (low + high) / 2;
|
||||
// 对左半部分归并排序
|
||||
MergeSort(data, low, mid);
|
||||
// 对右半部分归并排序
|
||||
MergeSort(data, mid + 1, high);
|
||||
// 最后一次归并全部
|
||||
Merge(data, low, mid, high);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -192,9 +192,9 @@
|
||||
|
||||
广度优先遍历过程:
|
||||
|
||||
+ 访问顶点v。
|
||||
+ 访问v的所有未被访问的邻接点。
|
||||
+ 依次从这些邻接点(在步骤②中访问的顶点)出发,访问它们的所有未被访问的邻接点; 依此类推,直到图中所有访问过的顶点的邻接点都被访问。
|
||||
1. 访问顶点v。
|
||||
2. 访问v的所有未被访问的邻接点。
|
||||
3. 依次从这些邻接点(在步骤二中访问的顶点)出发,访问它们的所有未被访问的邻接点; 依此类推,直到图中所有访问过的顶点的邻接点都被访问。
|
||||
|
||||
邻接矩阵实现时的时间复杂度为$O(\vert V\vert^2)$,邻接表实现时的时间复杂度为$O(\vert V\vert+\vert E\vert)$;空间复杂度为$O(\vert V\vert)$。
|
||||
|
||||
@@ -207,10 +207,10 @@
|
||||
|
||||
深度优先遍历过程:
|
||||
|
||||
+ 访问顶点v。
|
||||
+ 依次从v的未被访问的邻接点出发,对图进行深度优先遍历。
|
||||
+ 直至图中和v有路径相通的顶点都被访问。
|
||||
+ 若此时图中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到图中所有顶点均被访问过为止。
|
||||
1. 访问顶点v。
|
||||
2. 依次从v的未被访问的邻接点出发,对图进行深度优先遍历。
|
||||
3. 直至图中和v有路径相通的顶点都被访问。
|
||||
4. 若此时图中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到图中所有顶点均被访问过为止。
|
||||
|
||||
邻接矩阵实现时的时间复杂度为$O(\vert V\vert^2)$,邻接表实现时的时间复杂度为$O(\vert V\vert+\vert E\vert)$;空间复杂度为$O(\vert V\vert)$。
|
||||
|
||||
@@ -277,7 +277,7 @@
|
||||
1. 从$v_0$开始,初始化三个数组:标记各顶点是否已找到最短路径;最短路径长度;最短路径上的前驱。
|
||||
2. 遍历所有结点,找到还没确定最短路径,且最短路径长度值最小的的一个顶点,这就确定了下一个最短路径的结点,令其各顶点是否已找到最短路径的值为true。
|
||||
3. 检查所有邻接这个结点的其他结点,若其点还没有找到最短路径,则更新最短路径长度值与最短路径上前驱的值。
|
||||
4. 重复步骤2再次循环遍历所有结点并找到没确定最短路径则最短路径长度最小的顶点。
|
||||
4. 重复步骤二再次循环遍历所有结点并找到没确定最短路径则最短路径长度最小的顶点。
|
||||
|
||||
Dijkstra算法与Prim算法类似,都是优先与最短的路径结合。
|
||||
|
||||
@@ -331,13 +331,13 @@ AOV网:用DAG图表示一个工程,顶点表示活动,有向边$<v_i,v_j>$
|
||||
|
||||
1. 从AOV网中选择一个没有前驱的入度为0的顶点并输出。
|
||||
2. 从网中删除该顶点和所有以它为起点的有向边。
|
||||
3. 重复步骤1和2直到当前的AOV网为空或当前网中不存在无前驱的顶点为止。
|
||||
3. 重复步骤一和二直到当前的AOV网为空或当前网中不存在无前驱的顶点为止。
|
||||
|
||||
逆拓扑排序的实现:
|
||||
|
||||
1. 从AOV网中选择一个没有后继的出度为0的顶点并输出。
|
||||
2. 从网中删除该顶点和所有以它为起点的有向边。
|
||||
3. 重复步骤1和2直到当前的AOV网为空或当前网中不存在无前驱的顶点为止。
|
||||
3. 重复步骤一和二直到当前的AOV网为空或当前网中不存在无前驱的顶点为止。
|
||||
|
||||
### 关键路径
|
||||
|
||||
|
||||
@@ -11,17 +11,23 @@
|
||||
|
||||
## 插入排序
|
||||
|
||||
插入排序就是将选定的目标值插入到对应的位置。
|
||||
|
||||
### 直接插入排序
|
||||
|
||||
#### 直接插入排序的过程
|
||||
|
||||
每次将一个待排序的记录按其关键字大小插入到前面已排好序的子序列中,直到全部记录插入完成为止。
|
||||
|
||||
#### 直接插入排序的性能
|
||||
|
||||
空间复杂度为$O(1)$。
|
||||
|
||||
时间复杂度主要来自对比关键字,移动元素,若有n个元素,则需要n-1趟处理。
|
||||
时间复杂度主要来自对比关键字,移动元素,若有$n$个元素,则需要$n-1$趟处理。
|
||||
|
||||
最好情况是原本的序列就是有序的,需要n-1趟处理,每次只需要对比一次关键字,不用移动元素,时间复杂度为$O(n)$。
|
||||
最好情况是原本的序列就是有序的,需要$n-1$趟处理,每次只需要对比一次关键字,不用移动元素,时间复杂度为$O(n)$。
|
||||
|
||||
最坏情况是原本的序列是逆序的,需要n-1趟处理,第i趟处理需要对比关键字i+1次,移动元素i+2次,时间复杂度是$O(n^2)$。
|
||||
最坏情况是原本的序列是逆序的,需要$n-1$趟处理,第$i$趟处理需要对比关键字$i+1$次,移动元素$i+2$次,时间复杂度是$O(n^2)$。
|
||||
|
||||
所以平均时间复杂度是$O(n^2)$。
|
||||
|
||||
@@ -31,12 +37,16 @@
|
||||
|
||||
### 二分插入排序
|
||||
|
||||
#### 二分插入排序的过程
|
||||
|
||||
也称为折半插入排序,是对直接插入排序的优化,在寻找插入位置时使用二分查找的方式。
|
||||
|
||||
当data[mid]==data[i]时,为了保证算法的稳定性,会继续在mid所指位置右边寻找插入位置。
|
||||
|
||||
当low>high时停止折半查找,并将[low,i-1]内的元素全部右移,并把元素值赋值到low所指的位置。
|
||||
|
||||
#### 二分插入排序的性能
|
||||
|
||||
空间复杂度为$O(1)$。
|
||||
|
||||
二分插入排序是稳定的。
|
||||
@@ -45,12 +55,16 @@
|
||||
|
||||
### 希尔排序
|
||||
|
||||
#### 希尔排序的过程
|
||||
|
||||
希尔排序也是对直接插入排序的优化。直接插入排序对于基本有序的序列排序效果较好,所以就希望序列能尽可能基本有序。从而希尔排序的思想就是先追求表中元素部分有序,然后逐渐逼近全局有序。
|
||||
|
||||
先将整个待排序元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的),分别进行直接插入排序,然后缩小增量重复上述过程,直到增量为1。
|
||||
|
||||
增量序列的选择建议是第一趟选择元素个数的一半,后面不断缩小到原来的一半。
|
||||
|
||||
#### 希尔排序的性能
|
||||
|
||||
空间复杂度为$O(1)$。
|
||||
|
||||
而时间复杂度和增量序列的选择有关,目前无法使用属性手段证明确切的时间复杂度。最坏时间复杂度为$O(n^2)$,在某个范围内可以达到$O(n^{1.3})$。
|
||||
@@ -65,4 +79,258 @@
|
||||
|
||||
### 冒泡排序
|
||||
|
||||
从后往前或从前往后两两比较相邻元素的值,若逆序则交换这两个值,直到序列比较完。这个过程是一趟冒泡排序。每一趟都会让关键字最小或最大的一个元素到未排序队列的第一个或最后一个。
|
||||
#### 冒泡排序的过程
|
||||
|
||||
从后往前或从前往后两两比较相邻元素的值,若逆序则交换这两个值,如果相等也不交换,直到序列比较完。这个过程是一趟冒泡排序,第$i$趟后第i个元素会已经排序完成。每一趟都会让关键字最小或最大的一个元素到未排序队列的第一个或最后一个。一共需要$n-1$趟排序。
|
||||
|
||||
#### 冒泡排序的性能
|
||||
|
||||
空间复杂度为$O(1)$。
|
||||
|
||||
最好情况下即本身序列有序,则比较次数是n-1,交换次数是0,从而时间复杂度是$O(n)$。
|
||||
|
||||
最坏情况是逆序情况,比较次数和交换次数都是$\dfrac{n(n-1)}{2}$,所以时间复杂度是$O(n^2)$。
|
||||
|
||||
从而平均时间复杂度是$O(n^2)$。
|
||||
|
||||
冒泡排序是稳定的。
|
||||
|
||||
冒泡排序可以用于链表。
|
||||
|
||||
### 快速排序
|
||||
|
||||
快速排序在内部排序中的表现确实是最好的。排序过程类似于构建二叉排序树。
|
||||
|
||||
#### 快速排序的过程
|
||||
|
||||
取待排序序列中的某个元素pivot作为基准(一般取第一个元素),通过一趟排序,将待排元素分为左右两个子序列,左子序列元素的关键字均小于或等于基准元素的关键字,右子序列的关键字则大于基准元素的关键字,然后分别对两个子序列继续进行排序,直至整个序列有序。
|
||||
|
||||
1. 先选择个值做标杆,把标杆放入pivot,比标杆小的放左边,大的放右边。
|
||||
2. 初始时,令high指向序列最右边的值,low指向序列最左边的值。
|
||||
3. 然后从high开始,当遇到比标杆大的值时high--。
|
||||
4. 当遇到比标杆小的值,就取出这个值放入当前low所指的位置。
|
||||
5. 然后high不动,low++开始移动。
|
||||
6. 若low所指向的值比标杆小,low++。
|
||||
7. 若low所指向的值比标杆大,则放入当前high所指向的位置。
|
||||
8. 然后low不动,high--开始移动。回到步骤三开始执行。
|
||||
9. 当low=high时表示low和high之前的元素都比基准小,low和high之后的元素都比基准大,完成了一次划分。然后把基准元素放入low和high指向的位置。
|
||||
10. 不断交替使用low和high指针进行对比。对左右子序列进行同样的递归操作即可,从步骤三开始。若左右两个子序列的元素数量等于一,则无需再划分。
|
||||
|
||||
#### 快速排序的性能
|
||||
|
||||
由于快速排序使用了递归,所以需要递归工作栈,空间复杂度与递归层数相关,所以为$O(递归层数)$。
|
||||
|
||||
每一层划分只需要处理剩余的待排序元素,时间复杂度不超过$O(n)$,所以时间复杂度为$O(n*递归层数)$。
|
||||
|
||||
而快速排序会将所有元素组织成为二叉树,二叉树的层数就是递归调用的层数。所以对于$n$个结点的二叉树,最小高度为$\lfloor\log_2n\rfloor+1$,最大高度为$n$。
|
||||
|
||||
从而最好时间复杂度为$O(n\log_2n)$,最坏时间复杂度为$O(n^2)$,平均时间复杂度为$O(n\log_2n)$;最好空间复杂度为$O(\log_2n)$,最快空间复杂度为$O(n)$。
|
||||
|
||||
所以如果初始序列是有序的或逆序的,则快速排序的性能最差。若每一次选中的基准能均匀划分,则效率最高。
|
||||
|
||||
所以对于快速排序的性能优化是选择尽可能能中分的基准元素,入选头中尾三个位置的元素,选择中间值作为基准元素,或随机选择一个元素作为基准元素。
|
||||
|
||||
快速排序算法是不稳定的。
|
||||
|
||||
## 选择排序
|
||||
|
||||
选择排序就是每一趟在待排序元素中选取关键字最小或最大的元素加入有序子序列。
|
||||
|
||||
### 简单选择排序
|
||||
|
||||
#### 简单选择排序的过程
|
||||
|
||||
即每一趟在待排序元素中选取关键字最小的元素加入有序序列。
|
||||
|
||||
#### 简单选择排序的性能
|
||||
|
||||
空间复杂度为$O(1)$。
|
||||
|
||||
时间复杂度为$O(n^2)$。
|
||||
|
||||
简单选择排序是不稳定的。
|
||||
|
||||
简单选择排序也可以适用于链表。
|
||||
|
||||
### 堆排序
|
||||
|
||||
#### 堆的定义
|
||||
|
||||
若$n$个关键字序列$L$满足下面某一条性质,则就是堆:
|
||||
|
||||
1. 若满足$L(i)\geqslant L(2i)$且$L(i)\geqslant L(2i+1)\,(1\leqslant i\leqslant\dfrac{n}{2})$则是大根堆或大顶堆。
|
||||
2. 若满足$L(i)\leqslant L(2i)$且$L(i)\leqslant L(2i+1)\,(1\leqslant i\leqslant\dfrac{n}{2})$则是小根堆或小顶堆。
|
||||
|
||||
#### 堆的建立
|
||||
|
||||
其实堆就是顺序存储的完全二叉树。其中:
|
||||
|
||||
+ $i\leqslant\lfloor\dfrac{n}{2}\rfloor$的结点都是非终端结点。
|
||||
+ $i$的左孩子是$2i$。
|
||||
+ $i$的右孩子是$2i+1$。
|
||||
+ $i$的父结点是$\lfloor\dfrac{n}{2}\rfloor$。
|
||||
|
||||
所以建立根堆的过程是:
|
||||
|
||||
1. 从$t\lfloor\dfrac{n}{2}\rfloor$的结点开始往前遍历。
|
||||
2. 检查当前结点$i$与左孩子和右孩子是否满足根堆条件,若不满足则交换。
|
||||
+ 若是建立大根堆,检查是否满足根大于等于左、右结点,若不满足,则当前结点与更大的一个孩子互换。
|
||||
+ 若是建立小根堆,检查是否满足根小于等于左、右结点,若不满足,则当前结点与更小的一个孩子互换。
|
||||
3. 若元素互换破坏了下一级的堆,则采用同样的方法继续向下调整。
|
||||
+ 若是建立大根堆,则小的元素不断下坠。
|
||||
+ 若是建立小根堆,则大的元素不断下坠。
|
||||
|
||||
#### 堆排序的过程
|
||||
|
||||
1. 每一趟将堆顶元素加入子序列(堆顶元素与待排序序列中的最后一个元素交换)。此时后面的这个元素就排序好了。
|
||||
2. 此时待排序序列已经不是堆了,需要将其再次调整为堆(小元素或大元素不断下坠)。
|
||||
3. 重复步骤一二。
|
||||
4. 直到n-1趟处理后得到有序序列。基于大根堆的堆排序会得到递增序列,而基于小根堆的堆排序会得到递减序列。
|
||||
|
||||
#### 堆排序的性能
|
||||
|
||||
堆排序的存储就是它本身,不需要额外的存储空间,要么只需要一个用于交换或临时存放元素的辅助空间。所以空间复杂度为$O(1)$。
|
||||
|
||||
若树高为$h$,某结点在第$i$层,则将这个结点向下调整最多只需要下坠$h-i$层,关键字对比次数不超过$2(h-i)$次。
|
||||
|
||||
第$i$层最多$2^{i-1}$个结点,而只有第$1\cdots(h-1)$层的结点才可能需要下坠调整。所以调整时关键字对比次数不超过$\sum_{i=h-1}^12^{i-1}2(h-i)=\sum_{j=1}^{h-1}2^{h-j}j\leqslant2n\sum_{j=1}^{h-1}\dfrac{j}{2^j}\leqslant4n$。
|
||||
|
||||
所以建堆的过程中,关键字对比次数不超过$4n$,建堆的时间复杂度为$O(n)$。
|
||||
|
||||
堆排序中处理时根结点最多下坠$h-1$层,而每下坠一层,最多对比关键字两次,所以每一趟排序的时间复杂度不超过$O(h)=O(\log_2n)$,一共$n-1$趟,所以时间复杂度为$O(n\log_2n)$。所以总的时间复杂度也是$O(n\log_2n)$。
|
||||
|
||||
堆排序是不稳定的。
|
||||
|
||||
#### 堆的插入
|
||||
|
||||
新元素放到表尾,并与其$\lfloor\dfrac{i}{2}\rfloor$的父结点进行对比,若新元素比父元素更大(大根堆)或更小(小根堆),则二者互换,并保持上升,直到无法上升为止。
|
||||
|
||||
#### 堆的删除
|
||||
|
||||
被删除的元素用堆底元素代替,然后让这个元素不断下坠,直到无法下坠为止。
|
||||
|
||||
## 归并排序
|
||||
|
||||
归并是指把两个(二路归并)或多个(多路归并)已经有序的序列合并为一个。
|
||||
|
||||
该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
|
||||
|
||||
## 二路归并排序
|
||||
|
||||
二路归并排序比较常用,且基本上用于内部排序,多路排序多用于外部排序。
|
||||
|
||||
### 二路归并排序的过程
|
||||
|
||||
1. 把长度为$n$的输入序列分成两个长度为$\dfrac{n}{2}$的子序列。
|
||||
2. 对这两个子序列分别采用归并排序。
|
||||
3. 将两个排序好的子序列合并成一个最终的排序序列。
|
||||
|
||||
### 二路归并排序的性能
|
||||
|
||||
二路归并排序是一棵倒立的二叉树。
|
||||
|
||||
空间复杂度主要来自辅助数组,所以为$O(n)$,而递归调用的调用栈的空间复杂度为$O(\log_2n)$,总的空间复杂度就是为$O(n)$。
|
||||
|
||||
$n$个元素二路归并排序,归并一共要$\log_2n$趟,每次归并时间复杂度为$O(n)$,则算法时间复杂度为$O(n\log_2n)$
|
||||
|
||||
归并排序是稳定的。
|
||||
|
||||
## 分配排序
|
||||
|
||||
分配排序过程无须比较关键字,而是通过用额外的空间来“分配”和“收集”来实现排序,它们的时间复杂度可达到线性阶$O(n)$。简言之就是:用空间换时间,所以性能与基于比较的排序才有数量级的提高。
|
||||
|
||||
### 基数排序
|
||||
|
||||
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
|
||||
|
||||
#### 基数的定义
|
||||
|
||||
假设长度为$n$的线性表中每个结点$a_j$的关键字由$d$元组$(k_j^{d-1},k_j^{d-2},\cdots,k_j^1,k_j^0)$组成,其中$0\geqslant k_j^i\geqslant r-1\,(0\geqslant i\geqslant d-1)$,其中$r$就是基数。
|
||||
|
||||
#### 基数排序的过程
|
||||
|
||||
若是要得到递减序列:
|
||||
|
||||
1. 初始化:设置$r$个空辅助队列$Q_{r-1},Q_{r-2},\cdots,Q_0$。
|
||||
2. 按照每个关键字位**权重递增**的次序(个、十、百),对$d$个关键字位分别做分配和收集。
|
||||
3. 分配就是顺序扫描各个元素,若当前处理的关键字位为$x$,就将元素插入$Q_x$队尾。
|
||||
4. 收集就是把$Q_{r-1},Q_{r-2},\cdots,Q_0$各个队列的结点依次出队并链接在一起。
|
||||
|
||||
#### 基数排序的性能
|
||||
|
||||
基数排序基本上使用链式存储而不是一般的顺序存储。
|
||||
|
||||
需要$r$个辅助队列,所以空间复杂度为$O(r)$。
|
||||
|
||||
一趟分配$O(n)$,一趟收集$O(r)$,一共有$d$趟分配收集,所以总的时间复杂度为$O(d(n+r))$。
|
||||
|
||||
基数排序是稳定的。
|
||||
|
||||
#### 基数排序的应用
|
||||
|
||||
对于一般的整数排序是可以按位排序的,也可以处理一些实际问题,如根据人的年龄排序,需要从年月日三个维度分别设置年份的队列、月份的队列(1到12)、日的队列(1到31)。
|
||||
|
||||
所以基数排序擅长解决的问题:
|
||||
|
||||
1. 数据元素的关键字可以方便地拆分为$d$组,且$d$较小。
|
||||
2. 每组关键字的取值范围不大,即$r$较小。
|
||||
3. 数据元素个数$n$较大。
|
||||
|
||||
### 计数排序
|
||||
|
||||
作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
|
||||
|
||||
#### 计数排序的过程
|
||||
|
||||
1. 找出待排序的数组中最大和最小的元素。
|
||||
2. 统计数组中每个值为i的元素出现的次数,存入数组C的第i项。
|
||||
3. 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)。
|
||||
4. 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
|
||||
|
||||
当输入的元素是$n$个属于$[0,k]$的整数时,时间复杂度是$O(n+k)$,空间复杂度也是$O(n+k)$,其排序速度快于任何比较排序算法。
|
||||
|
||||
当$k$不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。
|
||||
|
||||
计数排序是稳定的。
|
||||
|
||||
### 桶排序
|
||||
|
||||
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
|
||||
|
||||
#### 桶排序的过程
|
||||
|
||||
1. 设置一个定量的数组当作空桶。
|
||||
2. 遍历输入数据,并且把数据一个一个放到对应的桶里去。
|
||||
3. 对每个不是空的桶进行排序。
|
||||
4. 从不是空的桶里把排好序的数据拼接起来。
|
||||
|
||||
#### 桶排序的性能
|
||||
|
||||
桶排序最好情况下使用线性时间$O(n)$,桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为$O(n)$。桶排序的平均时间复杂度为线性的$O(n+C)$,其中$C=n*(\log n-\log m)$,其中$m$代表桶划分的数量。
|
||||
|
||||
很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
|
||||
|
||||
桶排序是稳定的。
|
||||
|
||||
## 内部排序
|
||||
|
||||
算法种类|最好时间复杂度|平均时间复杂度|最好时间复杂度|空间复杂度|是否稳定
|
||||
:-----:|:------------:|:----------:|:------------:|:-------:|:------:
|
||||
直接插入排序|$O(n)$|$O(n^2)$|$O(n^2)$|$O(1)$|是
|
||||
冒泡排序|$O(n)$|$O(n^2)$|$O(n^2)$|$O(1)$|是
|
||||
简单选择排序|$O(n^2)$|$O(n^2)$|$O(n^2)$|$O(1)$|否
|
||||
希尔排序|$?$|$?$|$?$|$O(1)$|否
|
||||
快速排序|$O(n\log_2n)$|$O(n\log_2n)$|$O(n^2)$|$O(\log_2n)$|否
|
||||
堆排序|$O(n\log_2n)$|$O(n\log_2n)$|$O(n\log_2n)$|$O(1)$|否
|
||||
二路归并排序|$O(n\log_2n)$|$O(n\log_2n)$|$O(n\log_2n)$|$O(n)$|是
|
||||
基数排序|$O(d(n+r))$|$O(d(n+r))$|$O(d(n+r))$|$O(r)$|是
|
||||
|
||||
## 外部排序
|
||||
|
||||
### 外部排序的原理
|
||||
|
||||
磁盘的读写是以块为单位,数据读入内存后才能被修改,修改完成后还需要写回磁盘。
|
||||
|
||||
外部排序就是针对数据元素太多,无法一次性全部读入内存进行排序而进行处理的在外部磁盘进行的排序处理方式。
|
||||
|
||||
使用归并排序的方式,最少只用在内存分配三块大小的缓冲区即可堆任意一个大文件进行排序。然后对缓冲区里的数据进行内部排序。
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
1. 确定中缀表达式中各个运算符的运算顺序进行排序。
|
||||
2. 选择下一个运算符,按照**左操作数 右操作数 运算符**的方式组合一个个新的操作数。
|
||||
3. 如果还有运算符没有处理就重复2步骤。
|
||||
3. 如果还有运算符没有处理就重复步骤二。
|
||||
|
||||
如A+B*(C-D)-E/F就是ABCD-*+EF/-和ABCD-\*EF/-+。中缀转后缀的结果可以有不同的结果。
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
|
||||
1. 从左往右扫描下一个元素,直到处理所有元素。
|
||||
2. 若扫描到操作数则压入栈,并回到1,若扫描到运算符则执行3.
|
||||
3. 扫描到运算符则弹出两个栈顶元素,执行相应操作,运算结果压入栈,回到步骤1。
|
||||
3. 扫描到运算符则弹出两个栈顶元素,执行相应操作,运算结果压入栈,回到步骤一。
|
||||
4. 先出栈的是右操作数,后出栈的是左操作数。
|
||||
|
||||
使用栈进行中缀表达式求值的程序实现:
|
||||
@@ -86,7 +86,7 @@
|
||||
|
||||
1. 确定中缀表达式中各个运算符的运算顺序进行排序。
|
||||
2. 选择下一个运算符,按照**运算符 左操作数 右操作数**的方式组合一个个新的操作数。
|
||||
3. 如果还有运算符没有处理就重复2步骤。
|
||||
3. 如果还有运算符没有处理就重复步骤二。
|
||||
|
||||
遵循右优先原则,只要能计算右边就优先计算右边。
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
|
||||
1. 从右往左扫描下一个元素,直到处理所有元素。
|
||||
2. 若扫描到操作数则压入栈,并回到1,若扫描到运算符则执行3.
|
||||
3. 扫描到运算符则弹出两个栈顶元素,执行相应操作,运算结果压入栈,回到步骤1。
|
||||
3. 扫描到运算符则弹出两个栈顶元素,执行相应操作,运算结果压入栈,回到步骤一。
|
||||
4. 先出栈的是左操作数,后出栈的是右操作数。
|
||||
|
||||
### 递归
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
1. 初始化一个辅助队列。
|
||||
2. 根结点入队。
|
||||
3. 若队列非空,则队头结点出队,访问该结点,如果有并将其左右孩子入队。
|
||||
4. 重复步骤3直至队列空。
|
||||
4. 重复步骤三直至队列空。
|
||||
|
||||
### 遍历序列构造二叉树
|
||||
|
||||
@@ -416,7 +416,7 @@ AL(H) CL(H-1) CR(H) BR(H)
|
||||
1. 将这n个结点分别作为n棵仅含一个结点的二叉树,构成森林F。
|
||||
2. 构造一个新结点,从F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。
|
||||
3. 从F中删除刚才选出的两棵树,同时将新得到的树加入F中。
|
||||
4. 重复步骤2和3,直至F中只剩下一棵树为止。
|
||||
4. 重复步骤二和三,直至F中只剩下一棵树为止。
|
||||
|
||||
+ 每个初始结点最终都会变成叶子结点,且权值越小到根结点的路径长度越长。
|
||||
+ 哈夫曼树的结点总数为2n-1。
|
||||
|
||||
Reference in New Issue
Block a user