数据结构
相关文档
五、查找
-
静态查找表
- 查询某个特定的数据元素是否在查找表中; 2. 检索某个特定数据元素的各种属性。
-
动态查找表
- 在查找表中插入不存在的数据元素;2. 或者从查找表中删除已存在的某个数据元素。
-
平均查找长度
为确定记录在查找表中的位置, 需和给定关键字进行比较的次数的期望值称为查找算法在查找成功时的平均查找长度。
适用于数据量不大
:顺序查找、折半查找、分块查找、二叉排序树、平衡二叉树、哈希表
适用于数据巨大:B_树
1、静态查找表的查找方法
查找方法 | 数据格式 | 平均查找长度 | 适用于 |
---|---|---|---|
顺序查找法 | 无要求 | n + 1 2 \frac{n+1}{2} 2n+1 | 通用;n 较大时,查找效率较低 |
折半查找法 | 有序 | l o g 2 ( n + 1 ) − 1 log_{2}(n+1)-1 log2(n+1)−1 | 要求查找表以顺序存储 ,且按关键字有序排列 |
分块查找 | 块与块有序 | L b + L w L_b+L_w Lb+Lw | 块内不一定有序,但块与块之间是有序的。 L b L_b Lb 索引平均查找长度, L w L_w Lw 块内平均查找长度 |
2、动态查找表
表结构本身是在查找过程中动态生成的;即对于给定值 key,若表中存在关键字等于 key 的记录,则查找成功返回 ,否则插入关键字为 key 的记录。
二叉排序树
又称二叉查找树,它或是一个空树,或是具有以下性质的二叉树:
- 若左子树非空,则左子树所有节点的值均小于根节点;
- 若右子树非空,则右子树所有节点的值均大于根节点;
- 左右字节点本身是二叉排序树。
平衡二叉树
又称 AVL 树,它或是一个空树,或是具有以下性质的二叉树:它的左右子树都是平衡二叉树,且左子树和右子树的高度之差的绝对值不能超过 1。
插入操作失去平衡后调整规律:
- LL 型单向右旋转平衡处理;
- RR 型单向左旋转平衡处理;
- LR 型先左后右双向旋转平衡处理;
- RL 型先右后左双向旋转平衡处理。
B_树
一颗 m 介的 B_树,或空树,或是满足以下特性的 m 叉树
3、哈希表
冲突只能尽可能减少而不能避免。
六、排序
-
在待排序的一个序列中, R i R_i Ri 和 R j R_j Rj 的关键字相等,且排序前 R i R_i Ri 领先于 R j R_j Rj,那么排序后 R i R_i Ri 和 R j R_j Rj 的相对次序保持不变, 则称此类排序法为
稳定的
;若排序后 R j R_j Rj 可能领先于 R j R_j Rj ,则称此类排序为不稳定的
。 -
内部排序:待排序记录全部存放在内存中进行排序的过程。
-
外部排序:排序记录过大,以至内存不能容纳全部记录,在排序过程中尚需对外存进行访问的排序过程。
方法 | 稳定性 | 算法 | 时间复杂度 | 空间复杂度 |
---|---|---|---|---|
直接插入 排序 | 稳定 | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | |
冒泡 排序 | 稳定 | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | |
简单选择 排序 | 不稳定 | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | |
希尔 排序 | 不稳定 | O ( n 1.3 ) O(n^{1.3}) O(n1.3) | O ( 1 ) O(1) O(1) | |
快速 排序 | 不稳定 |
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2n) 若序列有序或基本有序时: O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | |
堆 排序 | 不稳定 | n l o g n nlogn nlogn | O ( 1 ) O(1) O(1) | |
归并 排序 | 稳定 | 分治法 | n l o g n nlogn nlogn | O ( n ) O(n) O(n) |
基数 排序 | 稳定 | O ( n ) 或 O ( ( d + k ) ∗ n ) O(n) 或 O((d+k)*n) O(n)或O((d+k)∗n) | O ( n + d ) O(n+d) O(n+d) |
简单排序
1、直接插入排序
在插入第 i 个记录时, R 1 、 R 2 、 … 、 R i − 1 R_1、R_2、…、R_{i-1} R1、R2、…、Ri−1已经排好序,这时将 R 1 R_1 R1 的关键字 k 1 k_1 k1 依次与关键字 k 1 、 k 2 k_1、k_2 k1、k2等进行比较,从而找到应该插入的位置并将 R i R_i Ri 插入,插入位置及其后的记录依次向后移动。
基本有序(升序)时,比较次数较少;逆序时比较次数最多。
代码示例(Java):
public InsertSort(int[] data) {
int n = data.length;
int tmp;
int i, j;
for (i = 1; i < n; i++) {
// Ri 小于 Ri-1,将 Ri 插入到前面的记录中, 否则继续比较下一个(i+1)记录。
if (data[i] < data[i - 1]) {
tmp = data[i]; // 将 Ri 暂存 tmp
// data[i] = data[i - 1];
// 将 Ri 依次与前面的记录比较(由后往前);若比较记录大于 Ri,则比较的记录向后移动一位
for (j = i - 1; j >= 0 && data[j] > tmp; j--) {
data[j + 1] = data[j];
}
data[j + 1] = tmp; // Rj <= tmp 时,Ri 插入到 j 后面 j+1 的位置
}
}
}
2、冒泡排序
每一轮比较关键字最大(或最小)的记录被交换到最后位置上。
代码示例(Java):
public BubbleSort(int[] data) {
if (data == null || data.length == 0) {
throw new IllegalArgumentException("输入的数组不能为空或者NULL");
}
int n = data.length;
boolean swapped = false;
// 外层循环控制排序轮数,每轮确保一个最大元素位置正确
for (int i = 0; i < n; i++) {
// 内层循环控制每轮排序中两两比较的次数
for (int j = 1; j < n - i; j++) {
// 如果前一个元素大于后一个元素,则交换位置,使得较大的元素逐渐向数组尾部移动
if (data[j - 1] > data[j]) {
int tmp = data[j];
data[j] = data[j - 1];
data[j - 1] = tmp;
swapped = true;
}
}
if (!swapped) {
break; // 如果当前轮没有发生交换,说明数组已经有序。
}
}
}
3、简单选择排序
(升序)每一轮排序都从待排序的序列(无序区)中选取一个最小值,并将其与无序区的第一个元素进行交换,此时有序区长度 +1,无序区长度 -1。重复上述过程直至整个序列有序排列。
代码示例(Java):
public SelectSort(int[] data) {
// 遍历整个数组,对每个元素进行排序
for (int i = 0; i < data.length; i++) {
int min = i; // 假设当前元素为最小值
// 在剩余未排序部分中寻找最小值
for (int j = i + 1; j < data.length; j++) {
if (data[min] > data[j]) {
min = j; // 更新最小值的索引
}
}
// 将找到的最小值与当前元素交换,确保当前元素是当前位置及之前的部分最小值
if (min != i) {
int tmp = data[min];
data[min] = data[i];
data[i] = tmp;
}
}
}
4、希尔排序
第一个突破 O ( n 2 ) O(n^2) O(n2) 的排序算法,是直接插入排序的改进版,它会优先比较距离较远的元素(又叫缩小增量排序)。
步骤:
- 先取一个小于 n n n 的整数 d 1 d_1 d1 作为第一个增量,把所有距离为 d 1 d_1 d1 倍数的序号(序号 % d 1 = 0 \%d_1 = 0 %d1=0)的记录放在同一个组中(即分成了 d 1 d_1 d1 个组),在各组内进行直接插入排序;
- 然后取第二个增量 d 2 ( d 2 < d 1 ) d_2(d_2<d_1) d2(d2<d1), 重复上述分组和排序工作;
- 依此类推,直到所取的增量 d i = 1 ( d 1 < d i − 1 < … < d 2 < d 1 ) d_i=1(d_1<d{i-1}<…<d_2<d_1) di=1(d1<di−1<…<d2<d1),即所有记录放在同一组进行一次直接插入排序。
代码示例(Java):
public ShellSort(int[] data) {
int n = data.length;
int tmp;
// 以 gap 为间隔逐步缩小,对数组进行分组并排序
for (int gap = n / 2; gap > 0; gap /= 2) {
// 对每个分组进行插入排序
for (int i = gap; i < n; i++) {
for (int j = i - gap; j >= 0; j -= gap) {
if (data[j] > data[j + gap]) {
tmp = data[j];
data[j] = data[j + gap];
data[j + gap] = tmp;
}
}
}
}
}
5、快速排序
代码示例(Java):
/**
* 对给定数组的指定部分进行分区操作,使得所有小于等于基准值的元素位于基准值左侧,所有大于基准值的元素位于基准值右侧。
*
* @param data 待分区的数组
* @param l 分区的起始位置
* @param r 分区的结束位置
* @return 分区后基准值所在位置的索引
*/
public int partition(int[] data, int l, int r) {
int i = l, j = r;
if (i < j) {
while (i < j) {
// 从右向左找到第一个小于基准值的数据,并与i位置的数据交换
while (i < j && data[j] >= data[l]) {
j--;
}
// 从左向右找到第一个大于基准值的数据,并与i位置的数据交换
while (i < j && data[i] <= data[l]) {
i++;
}
swapped(data, i, j); // 交换找到的数据对
}
}
swapped(data, i, l); // 将基准值放置到正确的位置
return i;
}
/**
* 快速排序算法
* 对指定的整型数组 data 中,从下标 l 到下标 r 的子数组进行排序
*
* @param data 要排序的整型数组
* @param l 排序子数组的左边界(包含)
* @param r 排序子数组的右边界(包含)
*/
public void QuickSort(int[] data, int l, int r) {
// 当左边界大于等于右边界时,表示子数组长度为 1,无需排序,终止递归
if (l >= r) {
return;
}
// 划分操作,返回枢轴元素最终位置(哨兵划分)
int point = partition(data, l, r);
// 递归地对左子数组进行快速排序
QuickSort(data, l, point - 1);
// 递归地对右子数组进行快速排序
QuickSort(data, point + 1, r);
}
6、堆排序
对于 n 个元素的关键字序列 k 1 , k 2 , . . . , k n {k_1,k_2,...,k_n} k1,k2,...,kn,当且仅当满足下列关系时称其为堆,其中 2 i 和 2 i + 1 2i 和 2i+1 2i和2i+1 均不大于 n
(小顶堆) { k i ≤ k 2 i k i ≤ k 2 i + 1 \begin{cases} k_i \leq k_{2i} \\ k_i \leq k_{2i+1}\end{cases} {ki≤k2iki≤k2i+1 或 (大顶堆) { k i ≥ k 2 i k i ≥ k 2 i + 1 \begin{cases} k_i \geq k_{2i} \\ k_i \geq k_{2i+1}\end{cases} {ki≥k2iki≥k2i+1
堆的存储结构可看做是一颗完全二叉树
,完全二叉树中非终端结点的值均不小于(或不大于)其左、右孩子结点。
代码示例(Java):
/**
* 堆排序算法
* 对给定的整型数组进行排序
*
* @param data 待排序的整型数组
*/
public void HeapSort(int[] data) {
int n = data.length;
int i;
// 步骤1:建立最大堆,从最后一个非叶子节点开始调整堆
// 从最后一个非叶子节点(即n/2-1)开始,向前遍历数组,对每个元素执行堆调整操作
for (i = n / 2 - 1; i >= 0; --i) {
heapAdjus(data, i, n - 1);
}
// 重复步骤2、3,直到整个数组有序。
for (i = n - 1; i > 0; --i) {
// 步骤2:将当前最大元素(根节点)与末尾元素交换
// 交换后的末尾元素不参与下一轮堆排序(已有序)。
swapped(data, 0, i);
// 步骤3:从第一个元素开始(有效长度为 i-1),逐步向后交换并调整堆。
heapAdjus(data, 0, i - 1);
}
}
/**
* 调整堆结构,保证父节点的值不小于其子节点的值。
*
* @param data 堆数组
* @param s 需要调整的节点索引
* @param m 堆数组的有效长度
*/
public void heapAdjus(int[] data, int s, int m) {
int tmp, j;
tmp = data[s]; // 保存当前节点的值
// 遍历子节点,找到合适的位置插入当前节点
for (j = 2 * s + 1; j <= m; j = 2 * j + 1) {
// 如果左子节点小于右子节点,将j指向右子节点
if (j < m && data[j] < data[j + 1]) {
++j;
}
// 如果当前节点值大于子节点值,结束循环
if (tmp > data[j]) {
break;
}
// 将子节点值赋给当前节点
data[s] = data[j];
s = j; // 更新当前节点索引,指向最大子节点索引
}
// 将保存的当前节点值插入最终位置
data[s] = tmp;
}
7、归并排序
是一种基于分治策略的算法。包含 “划分” 和 “合并” 阶段。
代码示例(Java):
/**
* 实现归并排序算法。
*
* @param data 待排序的整型数组。
* @param s 数组的起始索引。
* @param n 数组的终止索引。
* 说明:该函数不对返回值进行操作,排序是就地进行的,即直接对传入的数组进行排序。
*/
public void MergeSort(int[] data, int s, int n) {
int m;
// 当起始索引小于终止索引时,说明子数组长度大于1,需要继续分解并排序
if (s < n) {
// 计算当前子数组的中间索引
m = (s + n) / 2;
// 对左半部分进行递归排序
MergeSort(data, s, m);
// 对右半部分进行递归排序
MergeSort(data, m + 1, n);
// 合并左右两部分,完成排序
merge(data, s, m, n);
//System.out.printf("s = %d, m = %d, n = %d \r\n", s, m, n);
}
}
/**
* 将数组的两个部分合并成一个有序部分。
*
* @param data 要合并的原始数组;原始数组左右两部分有序。
* @param l 合并范围的左边界(包含)。
* @param mid 合并范围的中间位置(不包含)。
* @param r 合并范围的右边界(包含)。
*/
public void merge(int[] data, int l, int mid, int r) {
// 初始化指针i、j、k,分别指向左子数组、右子数组和临时数组的起始位置。
int i = l, j = mid + 1, k = 0;
// 创建一个临时数组用于存储合并后的结果。
int[] tmp = new int[r - l + 1];
// 循环,直到其中一个子数组的所有元素都被处理完毕。
while (i <= mid && j <= r) {
// 比较两个子数组的当前元素,将较小的元素放入临时数组。
if (data[i] < data[j]) {
tmp[k++] = data[i++];
} else {
tmp[k++] = data[j++];
}
}
// 处理左子数组剩余的元素。
while (i <= mid) {
tmp[k++] = data[i++];
}
// 处理右子数组剩余的元素。
while (j <= r) {
tmp[k++] = data[j++];
}
// 将合并后的结果复制回原始数组。
for (k = 0; k < tmp.length; k++) {
data[l + k] = tmp[k];
}
}