文章目录
- 1 声明
- 2 线性表
- 2.1 线性表的顺序存储结构—顺序表
- 2.2 线性表的链式存储结构—链表
- 2.3 比较
- 2.4 线性表的其他存储方法—静态链表和间接寻址
- 3 栈和队列
- 3.1 栈
- 3.1.1 栈的顺序存储结构—顺序栈
- 3.1.2 栈的链式存储结构—链栈
- 3.1.3 比较
- 3.2 队列
- 3.2.1 队列的顺序存储结构—循环队列
- 3.2.2 队列的链式存储结构—链队列
- 3.2.3 比较
- 4 字符串和多维数组
- 4.1 字符串
- 4.2 多维数组
- 5 树和二叉树
- 5.1 树的存储结构
- 5.2 二叉树的存储结构
- 6 图
- 6.1 图的存储结构
- 6.2 最小生成树
- 6.3 最短路径
- 6.4 有向无环图
- 7 查找技术
- 7.1 线性表查找技术
- 7.2 树表查找技术
- 7.2.1 二叉排序树
- 7.2.2 平衡二叉树
- 7.3 散列表查找技术
- 7.3.1 散列函数的设计
- 7.3.2 处理冲突的方法
- 8 排序技术
- 8.1 插入排序
- 8.1.1 直接插入排序
- 8.1.2 希尔排序
- 8.2 交换排序
- 8.2.1 起泡排序
- 8.2.2 快速排序
- 8.2.2.1 挖坑法快速排序
- 8.2.2.2 hoare法快速排序
- 8.3 选择排序
- 8.3.1 简单选择排序
- 8.3.2 堆排序
- 8.4 归并排序
- 8.5 分配排序
- 8.5.1 桶排序
- 8.5.2 基数排序
- 8.6 排序算法的比较
- 8.6.1 时间复杂度
- 8.6.2 稳定性
- 9 索引技术
- 9.1 线性索引技术
- 9.2 树形索引
- 9.2.1 2-3树
- 9.2.2 B 树
- 9.2.3 B+树
- 10 结语
1 声明
此博客内容总结自 清华大学出版社 数据结构(C++版),ISBN 978-7-302-24416-5,仅用于个人学习,代码均为本人编写。
2 线性表
定义:是n(n >= 0)个具有相同类型的数据元素的有限序列。
通常用一维数组来实现。
2.1 线性表的顺序存储结构—顺序表
时间复杂度:按位查找O(1);按值查找O(n);插入O(n);删除O(n)
注:插入和删除操作存在插入点(或删除点)之后的元素的平移。
2.2 线性表的链式存储结构—链表
1、单链表
template<class DataType>
struct Node
{
DataType data;
Node<DataType> * next;
};
设p为一个指针变量。
指针p指向某个Node类型的节点,该节点用 *p 表示,*p为节点变量,指针p指向的节点简称节点p。
p->data为存放的数据,p->next为存放后继节点地址的指针。e.g. p指向ai,那么p->next指向ai+1。
2、循环链表
把单链表终端节点的指针域由空指针改为指向头节点。
3、双链表
template<class DataType>
struct DulNode
{
DataType data;
DulNode<DataType> * prior, * next;
};
(1)、将新节点s插入到节点p后面
s->prior = p;
s->next = p->next;
p->next->prior = s;
p->next = s; //注意顺序
(2)、删除p节点
p->prior->next = p->next;
p->next->prior = p->prior;
free p;
2.3 比较
1、时间性能比较(时间复杂度)
按位置查找,顺序表的O(1)优于链表的O(n)。
插入和删除操作,链表的O(1)优于顺序表的O(n)。
2、空间性能比较
顺序表的存储空间利用率更高,但需要预分配存储空间,可能造成不充分利用。
2.4 线性表的其他存储方法—静态链表和间接寻址
1、静态链表
用数组来模拟单链表。
template<class DataType>
struct SNode
{
DataType data;
int next;// 后继元素所在的数组下标
};
2、间接寻址
原来是在数组中存储数据元素,间接寻址方法在数组中存储指向该数据元素的指针。
进行插入和删除操作时,与数组类似,但移动的是指针而不是数据元素,改进了时间性能。
3 栈和队列
3.1 栈
3.1.1 栈的顺序存储结构—顺序栈
1、top
top指向栈顶元素在数组中的位置。
栈空时,top = -1;栈满时,top = stack.size() - 1。
2、两栈共享空间
一个栈的栈底为数组的始端,另一个栈的栈底为数组的末端。
3.1.2 栈的链式存储结构—链栈
单链表的头部作为栈顶。
3.1.3 比较
时间复杂度一样。
空间复杂度:顺序栈要初始化一个固定的长度,链栈需要指针域,各有所长。
3.2 队列
队头出队,从队尾入队。
3.2.1 队列的顺序存储结构—循环队列
1、front和rear
front是队头元素的前一个位置,rear为队尾的位置。
因此,size为5的数组最多放四个元素,有一个为空,为front指向的位置。
2、读取队头元素
return data[(front+1) % QueueSize]
3、入队
rear = (rear+1) % QueueSize;
data[rear] = x;
4、出队
front = (front+1) % QueueSize;
return data[front];
5、判空
return rear == front;//true为空
6、判满
return (rear+1) % QueueSize == front;//true为满
3.2.2 队列的链式存储结构—链队列
入队:链表尾插
出队:头部删除
3.2.3 比较
与栈类似,但不存在两个队列共享一个数组(两个栈可以共享一个数组)。
4 字符串和多维数组
4.1 字符串
1、字符串的比较
在计算机系统中,每个字符都有唯一的数值表示,称为字符编码,ASCII码是最常见的一种。
"abcd" = "abcd";
"abc" < "abcd";
"abac" < "abae";
2、字符串的存储结构
C/C++中用 '/0’来表示串的结束,占一个空间。
3、模式匹配
在主串S中寻找子串T,若成功找到,返回T在S中的位置;失败则返回0。
(1)、BF算法
朴素暴力的算法,最坏时间复杂度O(mn)。
int i = 0, j = 0;
while(S[i] != '/0' && T[j] != '/0'){
if(S[i] == T[j]){ i++; j++; }
else{ i = i - j +1; j = 0; }
}
if(T[j] == '/0') return i-j+1;
else return 0;
(2)、KMP算法
相比BF算法,主串i指针无需回溯。匹配失败时,i不动,j = next[j]。并且next只与T有关。
匹配相等的前缀序列中,主串的某个后缀正好是模式的前缀,那么就可以将模式向后滑动到与这些相等字符对齐的位置,并从该位置继续比较。
时间复杂度O(m+n)
4.2 多维数组
1、矩阵的压缩存储—使用多维数组或链表存储矩阵
(1)、对称矩阵的压缩存储
只需存储下三角部分(包括主对角线),因此开辟一个n(n+1)/2大小的数组。
对于元素aij(i>=j),元素在数组中的下标 k = i(i-1)/2 + j-1;
(2)、三角矩阵的压缩存储
与对称矩阵类似,区别在于,例如上三角矩阵,那么除了上三角部分(包括对角线)其余部分都是一个常数,c。
因此开辟一个n(n+1)/2 + 1大小的数组,最后一位储存c。
(3)、对角矩阵的压缩存储
对角矩阵中,所有元素都集中在以主对角线为中心的带状区域,其余元素为0。
存储方式1:与上面两种矩阵类似,存在数组中。
存储方式2:将对角线立起来,存在二维数组中。
例如对角矩阵主对角线为m,带宽为n,则将其存在m * n的数组中。
aij = bts;
t = i; s = j-i+2;
(4)、稀疏矩阵的压缩存储
存储方式1:三元组顺序表
//存储非零元素的位置和值
struct element
{
int row, col;
DataType item;
};
struct SparseMatrix
{
element data[100];
int mu, nu, tu;//行数,列数,非零元个数
}
存储方式2:十字链表
在矩阵非零元素改变的情况下相比三元组顺序表存储更优。
每一个节点由三部分组成:
element(包括row, col, item)
right,指向同一行中的下一个节点
down,指向同一列中的下一个节点
HA可用于对某一行或某一列头指针的快速查找。
5 树和二叉树
5.1 树的存储结构
1、双亲表示法
用一维数组储存树的各个节点。数组中的一个元素对应树中的一个节点,数组元素包括节点的数据信息以及parent的数组下标。
template <class DataType>
struct PNode
{
DataType data;
int parent;
};
2、孩子表示法
1、多重链表表示法
用链表实现。每个节点包括一个数据域和多个指针域,指针域的个数等于树的度,指向孩子节点。
2、孩子链表表示法
可以反映兄弟关系,但查找parent比较困难。
struct CTNode
{
int child;
CTNode * next;
};
template <class DatType>
struct CBNode
{
DataType data;
CTNode * firstchild;
};
3、双亲孩子表示法结合双亲表示法和孩子链表表示法。
4、孩子兄弟表示法
二叉链表,除了数据域外,两个指针域分别指向第一个孩子和右兄弟。
template <class DataType>
struct TNode
{
DataType data;
TNode<DataType> * firstchild, * rightsib;
};
5.2 二叉树的存储结构
1、顺序存储
将二叉树按完全二叉树的结构存储,会造成很多空间的浪费。
2、二叉链表
template <class DataType>
struct BiNode
{
DataType data;
BiNode<DataType> * lchild, * rchild;
};
重点:
(1). 前中后序遍历递归算法
(2). 层序遍历:利用队列
3、三叉链表
相比二叉链表多了指向parent的指针。
4、线索二叉树
在二叉链表的基础上进行改进,将二叉链表的空指针改为指向前驱或后继的指针。
6 图
6.1 图的存储结构
1、邻接矩阵
也称数组表示法,用二维数组存储边的信息。
2、邻接表
类似树的孩子链表表示法,对于每个顶点,将所有邻接于它的顶点链成一个单链表。
3、十字链表
主要用于存储有向图。
4、邻接多重表
主要用于存储无向图
6.2 最小生成树
1、Prim算法
图中总的顶点为V,已生成的树的顶点为U,关键在于找到连接U和V-U的最短边来扩充生成树。
设连通网中有n个顶点,时间复杂度O(n2)。适合稠密图。
2、Kruskal算法
找最短边,如果属于两个不同的连通分量,则并入生成树。
设联通网中有e条边,时间复杂度O(eloge),适合稀疏网。
6.3 最短路径
1、Dijkstra算法
用于求单源点最短路径的问题。
2、Floyd算法
用于求每一对顶点之间的最短路径问题。
6.4 有向无环图
1、AOV网与拓扑排序
对AOV网进行拓扑排序
2、AOE网与关键路径
带权有向图。
关键路径的长度是最短工期。
7 查找技术
7.1 线性表查找技术
1、顺序查找
时间复杂度O(n)
2、折半查找
要求线性表中的记录必须有序,一般只适用于静态查找。
时间复杂度O(logn)
(1)、非递归算法
int BinSearch1(int r[], int n, int k){
low = 1, high = n;
while(low <= high){
mid = (low + high) / 2;
if(k < r[mid])
high = mid - 1;
else if (k > r[mid])
low = mid + 1;
else
return mid;
}
return 0;
}
2、递归算法
int BinSearch2(int r[], int low, int high, int k){
if(low > high)
return 0;
else{
mid = (low + high) / 2;
if(k < r[mid])
return BinSearch2(r, low, mid-1, k);
else if (k > r[mid])
return BinSearch2(r, mid+1, high, k);
else
return mid;
}
}
7.2 树表查找技术
7.2.1 二叉排序树
1、插入算法
逻辑:如果root是空树,那么将s作为根节点插入,如果s->data小于root->data,那么插入到root的左子树,反之插入右子树。
void InsertBST(BiNode<int> * root, BiNode<int> * s){
if(root == NULL) root = s;
else if(s->data < root->data) InsertBST(root->left, s);
else InsertBST(root->right, s);
}
2、构造函数
设所有节点的val存放在数组 r[ ] 中,对于每一个 r[i] ,申请一个数据域为 r[i] 的节点s,令s->left和s->right为空,并不断调用InsertBST。
3、删除节点
(1)、若p是叶子,那么直接删
(2)、若p只有左子树或右子树,那么就把p的左子树或右子树接到p原来的位置即可
(3)、若p的左右子树均不为空。将p右子树中的最小值s来顶替p。
并再分两种情况,第一种特殊情况:p右子树中的最小节点s就是p的右孩子(s一定没有左子树),那么直接把s的右子树接在p的右边。第二种更一般的情况,用s的右子树代替原来s的位置。
// 第三种情况
par = p;
s = p->left;
// 查找p右子树中最小的节点,为s
while(s->left != NULL){
par = s;
s = s->left;
}
p->val = s->val;
// 特殊情况
if(par == p) par->right = s->right;
// 一般情况
else par->left = s->right;
delete s;
4、查找算法
void SearchBST(BiNode<int> * root, int k){
if(root == NULL) return NULL;
else if(root->data == k) return root;
else if(root->data > k) return SearchBST(root->right, k);
else return SearchBST(root->left, k);
}
7.2.2 平衡二叉树
7.3 散列表查找技术
散列 = Hash
7.3.1 散列函数的设计
1、直接定址法
H(key) = a * k + b (a, b为常数)
单调均匀不会冲突但不常用,因为这个方法适用于事先知道关键码的分布的情况。
2、除数留余法
3、数字分析法
如果事先知道关键码并且关键码中有若干位分布较为均匀,可以用这几位来作为散列地址。
4、平方取中法
对关键码进行平方,取中间若干位作为散列地址,因为平方之后中间几位分布较为均匀。
5、折叠法
将关键码分为几部分,将这几部分叠加求和,作为散列地址。
7.3.2 处理冲突的方法
1、开放定址法
开放定址法指的是一旦关键码的散列地址产生了冲突,就去寻找下一个空的散列地址。如何寻找?线性探测法(从冲突的下一个位置开始,依次找),随机探测法(位移量是一个随机数列),二次探测法(探测第i次的散列地址Hi’ = (H(key) + d) % m)
由开放定址法处理冲突得到的散列表叫闭散列表。
2、拉链法
将所有散列地址相同的记录储存在一个单链表中。
8 排序技术
8.1 插入排序
8.1.1 直接插入排序
#include <iostream>
void InsertSort(int r[], int n) {
int i, j;
for (i = 1; i < n; i++) {
int pivot = r[i];
for (j = i - 1; r[j] > pivot; j--) {
r[j + 1] = r[j];
}
r[j + 1] = pivot;
}
}
int main() {
int r[8] = { 1,2,78,4,22,3,4,5 };
int n = sizeof(r) / sizeof(r[0]);
InsertSort(r, n);
for (int i = 0; i < n; i++) {
std::cout << r[i] << ' ';
}
std::cin.get();
}
8.1.2 希尔排序
是直接插入排序的升级版。
第一轮,每间隔d分为一组,共分为d组,对每一组进行直接插入排序;第二轮,间隔为d/2,还是对每一组进行直接插入排序;直到d=1。
#include <iostream>
void ShellSort(int r[], int n) {
int i, j;
for (int d = n / 2; d >= 1; d = d / 2) {
for (i = d; i < n; i++) {
int pivot = r[i];
for (j = i - d; r[j] > pivot && j > 0; j = j - d) {
r[j + d] = r[j];
}
r[j + d] = pivot;
}
}
}
int main() {
int r[8] = { 1,2,78,4,22,3,4,5 };
int n = sizeof(r) / sizeof(r[0]);
ShellSort(r, n);
for (int i = 0; i < n; i++) {
std::cout << r[i] << ' ';
}
std::cin.get();
}
8.2 交换排序
8.2.1 起泡排序
每趟排序之后,exchange位置之后的记录一定是有序的,用bound记录
#include <iostream>
#include <algorithm>
void BubbleSort(int r[], int n) {
int exchange = n-1, bound;
while (exchange != 0) {
bound = exchange;
exchange = 0;
for (int i = 0; i < bound; i++) {
if (r[i] > r[i + 1]) {
std::swap(r[i], r[i + 1]);
exchange = i;
}
}
}
}
int main() {
int r[8] = { 1,2,78,4,22,3,4,5 };
int n = sizeof(r) / sizeof(r[0]);
BubbleSort(r, n);
for (int i = 0; i < n; i++) {
std::cout << r[i] << ' ';
}
std::cin.get();
}
8.2.2 快速排序
8.2.2.1 挖坑法快速排序
每趟排序完成之后,i和j都指向pivot最终的位置。
#include <iostream>
#include <algorithm>
void QuickSort(int *r, int left, int right) {
if (left > right) return;
int i = left, j = right, pivot = r[left];
while (i < j) {
while (i < j && r[j] >= pivot) j--;
if (i < j) {
std::swap(r[i], r[j]);
i++;
}
while (i < j && r[i] <= pivot) i++;
if (i < j) {
std::swap(r[i], r[j]);
j--;
}
}
QuickSort(r, left, i - 1);
QuickSort(r, i + 1, right);
}
int main() {
int r[8] = { 1,2,78,4,22,3,4,5 };
int n = sizeof(r) / sizeof(r[0]);
QuickSort(r, 0, n-1);
for (int i = 0; i < n; i++) {
std::cout << r[i] << ' ';
}
std::cin.get();
}
8.2.2.2 hoare法快速排序
#include <iostream>
#include <algorithm>
void QuickSort(int *r, int left, int right) {
if (left > right) return;
int i = left, j = right, pivot = r[left];
while (i < j) {
while (i < j && r[j] >= pivot) j--;
while (i < j && r[i] <= pivot) i++;
std::swap(r[i], r[j]);
}
std::swap(r[i], r[left]);
QuickSort(r, left, i - 1);
QuickSort(r, i + 1, right);
}
int main() {
int r[8] = { 1,2,78,4,22,3,4,5 };
int n = sizeof(r) / sizeof(r[0]);
QuickSort(r, 0, n-1);
for (int i = 0; i < n; i++) {
std::cout << r[i] << ' ';
}
std::cin.get();
}
8.3 选择排序
8.3.1 简单选择排序
第一轮把最小的数放在r[0],第二轮把第二小的数放在r[1],以此类推。
#include <iostream>
#include <algorithm>
void SelectSort(int *r, int n) {
for (int i = 0; i < n - 1; i++) {
int index = i;
for (int j = i + 1; j < n; j++) {
if (r[j] < r[index]) index = j;
}
std::swap(r[index], r[i]);
}
}
int main() {
int r[8] = { 1,2,78,4,22,3,4,5 };
int n = sizeof(r) / sizeof(r[0]);
SelectSort(r, n);
for (int i = 0; i < n; i++) {
std::cout << r[i] << ' ';
}
std::cin.get();
}
8.3.2 堆排序
1、堆的定义
小根堆:每个节点的值都小于等于其左右孩子节点的值。
大根堆:每个节点的值都大于等于其左右孩子节点的值。
2、堆排序
首先初始化一个大顶堆,从最后一个父亲节点开始构造,最后的那些都是叶子节点,不用管。交换堆顶元素与最后一个元素,再重新构造大顶堆。
如何构造大顶堆?首先用dad指向要筛选的元素,son指向要筛选元素的左孩子,再比较要筛选元素的左右孩子,哪个孩子大,son就指向谁。如果r[son]比r[dad]大,那就交换。
#include <iostream>
#include <algorithm>
void Sift(int* r, int start, int end) {
int dad = start;
int son = dad * 2 + 1;
while (son <= end) {
if (son + 1 <= end && r[son] < r[son + 1]) son++;
if (r[dad] > r[son]) return;
else {
std::swap(r[dad], r[son]);
dad = son;
son = dad * 2 + 1;
}
}
}
void HeapSort(int *r, int n) {
for (int i = n / 2 -1; i >= 0; i--) {
Sift(r, i, n-1);
}
for (int i = n-1; i > 0; i--){
std::swap(r[0], r[i]);
Sift(r, 0, i-1);
}
}
int main() {
int r[8] = { 1,2,78,4,22,3,4,5 };
int n = sizeof(r) / sizeof(r[0]);
HeapSort(r, n);
for (int i = 0; i < n; i++) {
std::cout << r[i] << ' ';
}
std::cin.get();
}
8.4 归并排序
#include <iostream>
int tmp[100];
void MergeSort(int* r, int left, int right) {
if (left == right) return;
int mid = (left + right) / 2;
MergeSort(r, left, mid);
MergeSort(r, mid + 1, right);
int i = left;
int j = mid + 1;
for (int k = left; k <= right; k++) {
if((j > right) || (i <= mid && r[i] <= r[j])){
tmp[k] = r[i];
i++;
}
else {
tmp[k] = r[j];
j++;
}
}
for (int k = left; k <= right; k++) {
r[k] = tmp[k];
}
}
int main() {
int r[8] = { 1,2,78,4,22,3,4,5 };
int n = sizeof(r) / sizeof(r[0]);
MergeSort(r, 0, n-1);
for (int i = 0; i < n; i++) {
std::cout << r[i] << ' ';
}
std::cin.get();
}
8.5 分配排序
8.5.1 桶排序
8.5.2 基数排序
#include <iostream>
#include <algorithm>
int GetMaxBit(int* r, int n){
int max = 0;
for (int i = 0; i < n; i++) {
max = std::max(r[i], max);
}
int bit = 1;
while (max / 10 != 0) {
max = max / 10;
bit++;
}
return bit;
}
void RadixSort(int* r, int n) {
int bit = GetMaxBit(r, n);
int base = 1;
while (bit--) {
int count[10] = { 0 };
int start[10] = { 0 };
int tmp[10] = { 0 };
for (int i = 0; i < n; i++) {
int index = r[i] / base % 10;
count[index]++;
}
for (int i = 1; i < 10; i++) {
start[i] = count[i - 1] + start[i - 1];
}
for (int i = 0; i < n; i++) {
int index = r[i] / base % 10;
tmp[start[index]++] = r[i];
}
for (int i = 0; i < n; i++) {
r[i] = tmp[i];
}
base *= 10;
}
}
int main() {
int r[8] = { 1,2,78,4,22,3,4,5 };
int n = sizeof(r) / sizeof(r[0]);
RadixSort(r, n);
for (int i = 0; i < n; i++) {
std::cout << r[i] << ' ';
}
std::cin.get();
}
8.6 排序算法的比较
8.6.1 时间复杂度
最好情况下,直接插入排序和起泡排序最快;
平均情况下,快速排序最快;
最坏情况下,堆排序和归并排序最快。
8.6.2 稳定性
稳定的:直接插入、起泡、归并、基数;
不稳定的:希尔、快排、简单选择、堆排。
9 索引技术
9.1 线性索引技术
9.2 树形索引
9.2.1 2-3树
特点:一个节点包含一个或两个关键码,每个节点有两个或三个孩子,所有叶子都在同一层。
对于树的各种操作都与二叉树类似。
9.2.2 B 树
B树是2-3树的推广,2-3树是3阶B树。
9.2.3 B+树
在B树的基础上按关键码的大小次序链起各个终端节点,形成单链表,并设置头指针。
10 结语