mirror of
https://github.com/Didnelpsun/CS408.git
synced 2026-06-18 09:27:23 +08:00
更新排序
This commit is contained in:
@@ -153,6 +153,7 @@
|
||||
<ClInclude Include="link_stack.h" />
|
||||
<ClInclude Include="link_string.h" />
|
||||
<ClInclude Include="link_tree.h" />
|
||||
<ClInclude Include="search.h" />
|
||||
<ClInclude Include="sequence_list.h" />
|
||||
<ClInclude Include="sequence_string.h" />
|
||||
<ClInclude Include="static_link_list.h" />
|
||||
|
||||
79
Code/sort.h
Normal file
79
Code/sort.h
Normal file
@@ -0,0 +1,79 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include "head.h"
|
||||
|
||||
// 直接插入排序
|
||||
int DirectInsertSort(element_type data[], int length) {
|
||||
int i, j;
|
||||
element_type temp;
|
||||
// 循环遍历数组
|
||||
for (i = 1; i < length; i++) {
|
||||
// 若data[i]关键字小于前一个
|
||||
if (data[i] < data[i - 1]) {
|
||||
// 用temp暂存data[i]
|
||||
temp = data[i];
|
||||
// 检查所有前面已经排好序的元素
|
||||
for (j = i - 1; j >= 0 && data[j] > temp; --j) {
|
||||
// 若元素大于temp则后移一位
|
||||
data[j + 1] = data[j];
|
||||
}
|
||||
// 赋值到插入位置
|
||||
data[j + 1] = temp;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 折半查找排序
|
||||
int BinaryInsertSort(element_type data[], int length) {
|
||||
int i, j, low, high, mid;
|
||||
// 依次将data[2]到data[n-1]插入到前面已经排序的序列中
|
||||
for (i = 2; i <= length; i++) {
|
||||
// 将data[i]暂存到data[0]
|
||||
data[0] = data[i];
|
||||
// 设置折半查找的范围
|
||||
low = 1;
|
||||
high = i - 1;
|
||||
while (low <= high) {
|
||||
// 取中间点
|
||||
mid = (low + high) / 2;
|
||||
if (data[mid] > data[0]) {
|
||||
// 查找左半子表
|
||||
high = mid - 1;
|
||||
}
|
||||
else {
|
||||
// 查找右半子表
|
||||
low = mid + 1;
|
||||
}
|
||||
}
|
||||
// 统一后移元素,空出插入位置
|
||||
for (j = 1; j >= high + 1; --j) {
|
||||
data[j + 1] = data[j];
|
||||
}
|
||||
// 插入元素
|
||||
data[high + 1] = data[0];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 希尔排序
|
||||
int ShellSort(element_type data[], int length) {
|
||||
// d代表当前处理的增量值
|
||||
int d, i, j;
|
||||
// data[0]只是暂存数据,等j<=0时就到了插入位置
|
||||
for (d = length / 2; d >= 1; d = d / 2) {
|
||||
for (i = d + 1; i <= length; ++i) {
|
||||
// 需要将data[i]插入到有序增量子表
|
||||
if (data[i] < data[i - d]) {
|
||||
// 暂存到data[0]中
|
||||
data[0] = data[i];
|
||||
for (j = i - d; j > 0 && data[0] < data[j]; j -= d) {
|
||||
// 记录后移,寻找插入的位置
|
||||
data[j + d] = data[j];
|
||||
}
|
||||
// 插入数据
|
||||
data[j + d] = data[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,3 +190,68 @@ B+树可以顺序查找,在叶子结点的块之间定义指向后面叶子结
|
||||
## 散列表查找
|
||||
|
||||
### 散列表定义
|
||||
|
||||
散列表又称哈希表,是一种数据结构,数据元素的关键字与其存储地址直接相关。一个散列结构是一块地址连续的存储空间。
|
||||
|
||||
### 散列函数
|
||||
|
||||
关键字与地址通过散列函数(哈希函数)来实现映射。即记录位置=散列函数(记录关键字)。
|
||||
|
||||
+ 直接定址法:可表示为散列函数(关键字)=a*关键字+b,其中a、b均为常数。这种方法计算特别简单,并且不会发生冲突,但当关键字分布不连续时,会出现很多空闲单元,会将造成大量存贮单元的浪费。
|
||||
+ 数字分析法:分析关键字的各个位的构成,截取其中若干位作为散列函数值,尽可能使关键字具有大的敏感度,即最能进行区分的关键字位,这些位数都是连续的。
|
||||
+ 平方取中法:先求关键字的平方值,然后在平方值中取中间几位为散列函数的值。因为一个数平方后的中间几位和原数的每一位都相关,因此,使用随机分布的关键字得到的记录的存储位置也是随机的。适用于关键字的每位取值都不够均匀或均小于散列地址所需的位数。
|
||||
+ 折叠法:将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为散列函数的值,称为折叠法。例如,假设关键字为某人身份证号码430104681015355,则可以用4位为一组进行叠加,即有5355+8101+1046+430=14932,舍去高位,则有H(430104681015355)=4932。
|
||||
+ 随机数法:对于存储位置给定随机数安排,查找起来会很麻烦。
|
||||
+ 除留余数法:散列函数(关键字)=关键字%p,p一般是不大于表长的最大质数。这种方法使用较多,关键是选取较理想的p值,使得每一个关键字通过该函数转换后映射到散列空间上任一地址的概率都相等,从而尽可能减少发生冲突的可能性。一般情形下,取p为一个素数较理想,如果是合数则因为可以被多个数整除从而多个关键字余数相同造成冲突。
|
||||
|
||||
### 映射冲突
|
||||
|
||||
一般情况下,设计出的散列函数很难是单射的,即不同的关键字对应到同一个存储位置,这样就造成了冲突(碰撞)。此时,发生冲突的关键字互为同义词。
|
||||
|
||||
#### 开放地址法
|
||||
|
||||
可存放新表项的空闲地址既向同义词开放叶向非同义词开放。从发生冲突的那个单元开始,按照一定的次序,从哈希表中找出一个空闲的存储单元,把发生冲突的待插入关键字存储到该单元中,从而解决冲突。既指如果当前冲突,则将元素移动到其他空闲的地方。
|
||||
|
||||
$H_i=(H(key)+d_i)\mod m$。
|
||||
|
||||
+ $i$表示发生第$i$次冲突,$i=1,2,\cdots,m-1)$。
|
||||
+ $m$为散列表长度,类似于循环队列,超出表长以后就循环到最左边。
|
||||
+ $d_i$为增量序列,是指发生第i次冲突的时候,H(key)往后偏移了多少位。
|
||||
|
||||
增量序列选择:
|
||||
|
||||
+ 线性探测再散列:$d_i=1,2,3,\cdots,m-1$。线性探测再散列法充分利用了哈希表的空间,但在解决一个冲突时,可能造成新的冲突(聚集:同义和非同义关键字都堆积到一起)。另外,也不能随便对结点进行删除。
|
||||
+ 二次探测再散列:$di=1,-1,2^2,-2^2\cdots,(\dfrac{m}{2})^2,-(\dfrac{m}{2})^2$。对比线性探测法更不容易产生聚集问题。<span style="color:orange">注意:</span>散列表长度$m$必须是一个可以表示为$4j+3$的素数才能探测到所有位置。
|
||||
+ 伪随机探测再散列,定义$d_i$是一个伪随机数。
|
||||
|
||||
#### 链地址法
|
||||
|
||||
又称为拉链法或链接法,是把相互发生冲突的同义词用一个单链表链接起来,若干组同义词可以组成若干个单链表。思想类似于邻接表的基本思想,且这种方法适合于冲突比较严重的情况。
|
||||
|
||||
指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。
|
||||
|
||||
#### 再散列法
|
||||
|
||||
再散列法或再哈希法其实很简单,就是再使用散列函数去散列一个输入的时候,输出是同一个位置就再次使用另一个散列函数计算,直至不发生冲突:
|
||||
|
||||
$H_i=RH_i(Key)\,i=1,2,\cdots,k$。
|
||||
|
||||
每次冲突都要重新哈希,计算时间增加。
|
||||
|
||||
#### 公共溢出区法
|
||||
|
||||
为所有冲突的关键字记录建立一个公共的溢出区来存放。在查找时,对给定关键字通过散列函数计算出散列地址后,先与基本表的相应位置进行比对,如果相等,则查找成功;如果不相等,则到溢出表进行顺序查找。如果相对于基本表而言,在有冲突的数据很少的情况下,公共溢出区的结构对查找性能来说还是非常高的。
|
||||
|
||||
### 散列查找
|
||||
|
||||
先通过散列函数计算目标元素存储地址,然后根据解决冲突的方法进行下一步的查询。
|
||||
|
||||
如果使用拉链法通过散列函数计算得到存储地址为空,则可以直接代表查找失败,这时候一般定义查找长度这里不算。
|
||||
|
||||
而如果使用开放地址法计算得到空位置的时候,代表查找失败,但是这时候需要定义查找长度要算这个地址。
|
||||
|
||||
若散列函数设计得足够好,散列查找时间复杂度可以达到$O(1)$,即不存在冲突。
|
||||
|
||||
**装填因子**=表中记录数/散列表长度。装填因子代表一个散列表中的满余情况,越大则查找效率越低。
|
||||
|
||||
若只给出了装填因子$\alpha$,则此时平均查找长度为:$ASL=\dfrac{1}{2}(1+\dfrac{1}{1-\alpha})$。
|
||||
|
||||
68
Data-Structrue/sort.md
Normal file
68
Data-Structrue/sort.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# 排序
|
||||
|
||||
## 基本概念
|
||||
|
||||
+ 排序:将一个数据元素的任意序列重新排列成一个按关键字有序的序列。
|
||||
+ 内部排序:待排序的记录存放在计算机的内存中所进行的排序操作称为内部排序。
|
||||
+ 外部排序:待排序的记录数量很大,以致内存一次不能容纳全部记录,在排序过程中需要访问外存的排序过程称为外部排序。
|
||||
+ 稳定的排序:比如一个序列是“1,4,3,3*,2”,按从小到大排序后变成“1,2,3,3*,4”,就叫做稳定排序,即3和3*相对顺序不变。如果相同关键字的顺序发生了改变,则是不稳定的排序。稳定性的需要看具体的应用场景。
|
||||
+ 内部排序的算法性能取决于算法的时间复杂度和空间复杂度,而时间复杂度一般是由比较和移动次数决定的。外部排序除此之外还要考虑磁盘读写速度和次数。
|
||||
+ 大部分排序算法都仅适用于顺序存储的线性表。
|
||||
|
||||
## 插入排序
|
||||
|
||||
### 直接插入排序
|
||||
|
||||
每次将一个待排序的记录按其关键字大小插入到前面已排好序的子序列中,直到全部记录插入完成为止。
|
||||
|
||||
空间复杂度为$O(1)$。
|
||||
|
||||
时间复杂度主要来自对比关键字,移动元素,若有n个元素,则需要n-1趟处理。
|
||||
|
||||
最好情况是原本的序列就是有序的,需要n-1趟处理,每次只需要对比一次关键字,不用移动元素,时间复杂度为$O(n)$。
|
||||
|
||||
最坏情况是原本的序列是逆序的,需要n-1趟处理,第i趟处理需要对比关键字i+1次,移动元素i+2次,时间复杂度是$O(n^2)$。
|
||||
|
||||
所以平均时间复杂度是$O(n^2)$。
|
||||
|
||||
直接插入排序算法是稳定的。
|
||||
|
||||
如果使用链表实现直接插入排序,移动元素的次数变少了,但是关键字对比次数仍然时$O(n^2)$,从而整体时间复杂度依然是$O(n^2)$。
|
||||
|
||||
### 二分插入排序
|
||||
|
||||
也称为折半插入排序,是对直接插入排序的优化,在寻找插入位置时使用二分查找的方式。
|
||||
|
||||
当data[mid]==data[i]时,为了保证算法的稳定性,会继续在mid所指位置右边寻找插入位置。
|
||||
|
||||
当low>high时停止折半查找,并将[low,i-1]内的元素全部右移,并把元素值赋值到low所指的位置。
|
||||
|
||||
空间复杂度为$O(1)$。
|
||||
|
||||
二分插入排序是稳定的。
|
||||
|
||||
比起直接插入排序,比较关键字的次数减少,移动元素的次数没变,所以总体时间复杂度为$O(n^2)$。
|
||||
|
||||
### 希尔排序
|
||||
|
||||
希尔排序也是对直接插入排序的优化。直接插入排序对于基本有序的序列排序效果较好,所以就希望序列能尽可能基本有序。从而希尔排序的思想就是先追求表中元素部分有序,然后逐渐逼近全局有序。
|
||||
|
||||
先将整个待排序元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的),分别进行直接插入排序,然后缩小增量重复上述过程,直到增量为1。
|
||||
|
||||
增量序列的选择建议是第一趟选择元素个数的一半,后面不断缩小到原来的一半。
|
||||
|
||||
空间复杂度为$O(1)$。
|
||||
|
||||
而时间复杂度和增量序列的选择有关,目前无法使用属性手段证明确切的时间复杂度。最坏时间复杂度为$O(n^2)$,在某个范围内可以达到$O(n^{1.3})$。
|
||||
|
||||
希尔排序是不稳定的。
|
||||
|
||||
希尔排序只适用于顺序表而不适合用于链表,无法快速进行增量的访问。
|
||||
|
||||
## 交换排序
|
||||
|
||||
交换排序即根据序列中两个元素关键的比较结构然后交换这两个记录在序列中的位置。
|
||||
|
||||
### 冒泡排序
|
||||
|
||||
从后往前或从前往后两两比较相邻元素的值,若逆序则交换这两个值,直到序列比较完。这个过程是一趟冒泡排序。每一趟都会让关键字最小或最大的一个元素到未排序队列的第一个或最后一个。
|
||||
Reference in New Issue
Block a user