路漫漫其修远兮,吾将上下而求索
1. 绪论
1.1 基本概念和术语
数据
数据元素
数据对象【数据元素的集合】
数据类型
- 原子类型
- 结构类型
- 抽象数据类型
数据结构
数据结构是相互之间存在一种或多种特定关系的数据元素的集合
逻辑结构
存储结构
数据的运算
1.2 数据结构三要素
- 逻辑结构
包括线性结构【一对一】:一般线性表、受限线性表【栈、队列、串】、线性表推广【数组】
非线性结构【一对多或同属一个集合】:集合、树形结构【一般树、二叉树、B+树】、图状结构【有向图、无向图,网状结构】
- 存储结构【物理】
顺序存储、链式存储、索引存储、散列存储【hash】
- 数据的运算
运算的定义【逻辑结构】和实现【存储结构】
1.3 算法
<1> 五个特性
- 有穷性
- 确定性
- 可行性
- 输入
- 输出
<2>效率的度量
- 时间复杂度
最坏时间复杂度,平均时间复杂度,最好时间复杂度
- 空间复杂度
2. 线性表
2.1 定义
线性表是具有相同数据类型的n(n >= 0)个数据元素的优先序列
2.2 线性表的基本操作
1 | InitList(&L) //初始化表 |
2.3 顺序表【数组】
线性表的顺序存储【随机存储的存储结构】,其逻辑顺序与其物理顺序相同,线性表中元素的位序是从1开始的,高级程序设计语言数组的下标从0开始
- 顺序表不适合插入和删除
2.4 单链表
线性表的链式存储
1 | typedef struct LNode { //定义单链表结点类型 |
- 采用头插法建立单链表
1 | LinkList List_HeadInsert(LinkList &L) {//逆向建立单链表 |
缺点和优点:简单,生成结点的次序和输入数据的顺序不一致
- 采用尾插法建立单链表
1 | LinkList List_TailInsert(LinkList &L) {//正向建立单链表 |
- 按序号查找节点值
1 | LNode *GetElem(LinkList L,int i) { |
- 按值查找表节点
1 | LNode *LocateElem(LinkList L,ElemType e) { |
- 插入节点操作
1 | p = GetElem(L,i-1); |
- 对某一结点进行前插操作
1 | s -> next = p -> next; |
- 删除节点操作
1 | p = GetElem(L,i-1);//查找删除位置的前驱结点 |
- 删除结点 *p
1 | q = p->next; |
- 求表长操作
带头结点和不带头结点是不同的
1 | int count = 0;//计数器 |
2.5 双链表
线性表的链式存储,增加了指向前驱的prior指针,双链表按位查找和按值查找与单链表相同,插入和删除的实现上有不同
1 | typedef struct DNode { |
- 双链表的插入操作
1 | s->next = p->next;//将结点 *s 插入到 *p结点之后 |
- 双链表的删除操作
1 | p->next = q->next; |
2.6 循环链表
2.6.1 循环单链表
最后一个结点的指针不是NULL,而改为指向头结点,判空判断是否为头指针,插入和删除和单链表一致,无需判断是否为表尾
2.6.2 循环双链表
在循环双链表中,某结点 *p为尾结点时,p->next == L;当循环双链表为空表时,其头结点的prior和next都等于L;
2.7 静态链表
静态链表借助数组来描述线性表的链式存储结构,结点也有data和next,next表示为相对地址【相当于数组下标】,叫游标,以next == -1 作为结束的标志
1 |
|
2.8 顺序表和链表的比较
2.8.1 存储(读写)方式
顺序表可以顺序存取,也可以随机存取,链表只能从表头顺序存取
2.8.2 逻辑与物理结构
顺序存储,逻辑相邻,物理存储也相邻;链式存储,逻辑相邻,物理存储不一定相邻
2.8.3 查找、插入和删除操作
2.8.4 空间分配
顺序存储在静态存储分配情形下,不能够扩充,动态存储扩充需要移动大量的元素
2.9 栈和队列
2.9.1 栈的属性
栈是只允许在一端进行插入和删除操作的受限线性表,其操作的特性是后进先出【Last In First Out】LIFO
- 栈顶
- 栈底
- 空栈
2.9.2 栈的基本操作
1 | InitStack(&S) //初始化一个栈 |
2.9.3 栈的顺序存储
- 顺序栈的实现
1 |
|
栈顶指针 :S.top,初始化时设置S.top = -1 或者S.top = 0,栈顶元素:S.data[S.top]
- 初始化
1 | void InitStack(SqStack &S) { |
- 判栈空
1 | bool StackEmpty(SqStack S) { |
- 进栈
1 | bool Push(SqStack &S,ElemType x) { |
- 出栈
1 | bool Pop(SqStack &S,ElemType &x) { |
- 读栈顶元素
1 | bool GetTop(SqStack &S,ElemType &x) { |
2.9.4 共享栈
利用栈底位置相对不变的特性,可让两个顺序栈共享一个一维数组空间
2.9.5 栈的链式存储结构
- 链式存储
1 | typedef struct Linknode{ |
入栈与出栈都在链表的表头进行
2.9.6 队列的属性
队列简称队,也是一种操作受限的线性表,只允许在表的一端进行插入,而在表的另一端进行删除,其操作特性是先进先出【First In First Out】FIFO
- 队列基本操作
1 | InitQueue(&Q) //初始化队列 |
2.9.7 队列的顺序存储
- 顺序队的实现
1 |
|
初始状态(队空条件):Q.front == Q.rear == 0
【入】进队操作:队不满时,先送值到队尾元素,队尾指针+1
【出】离队操作:队不空时,先取队头元素值,再将队头指针+1
顺序存储可能会造成假溢出的情况
2.9.8 循环队列
- 初始化
1 | void InitQueue(SqQueue &Q) { |
- 判队空
1 | bool isEmpty(SqQueue Q) { |
- 入队
1 | bool EnQueue(SqQueue &Q,ElemType x) {//入队 |
- 出队
1 | bool DeQueue(SqQueue &Q,ElemType &x) { |
2.9.9 队列的链式存储
- 链式存储的定义
1 | typedef struct{//链式队列结点 |
- 初始化
1 | void InitQueue(LinkQueue &Q) { |
- 判队空
1 | bool isEmpty(LinkQueue Q) { |
- 入队
1 | void EnQueue(LinkQueue &Q,ElemType e) { |
- 出队
1 | bool DeQueue(LinkQueue &Q,ElemType &x) { |
2.9.10 双端队列
双端队列是指允许两端都可以进行入队和出队的操作,逻辑结构仍是线性结构
2.9.11 栈和队列的应用
栈在括号匹配的应用
栈在表达式求值中的应用
栈在递归中的应用【通常情况下,减少了代码量,但效率不是太高】
队列在层次遍历中的应用
队列在计算机系统的应用
- 解决主机和外部设备之间速度不匹配的问题
- 解决多用户引起的资源竞争问题
2.9.12 特殊矩阵的压缩存储
- 对称矩阵
- 三角矩阵
- 三对角矩阵
- 洗漱矩阵
3. 串
3.1 串的概念和属性
串是由零个或者多个字符组成的有限序列
串中任意多个连续的字符组成的子序列称为该串的子串,包含子串的串称为主串
定长顺序存储表示
1 |
|
- 堆分配存储表示
1 | typedef struct { |
- 块链存储表示
类似于线性表的链式存储结构,也可采用链表方式的存储串值,最后一个结点占不满时用#补上;
3.2 串的基本操作
1 | StrAssign(&T,chars)//赋值操作 |
3.3 串的模式匹配
- 简单的模式匹配算法
1 | int Index(SString S,SString T) { |
- KMP算法
1 | int Index_KMP(String S,String T,next[]) { |
- KMP算法进一步-nextval数组
1 | int Index_KMP(String S,String T,next[]) { |
4. 树与二叉树
4.1 树的概念和属性
树是指n(n>=0)个结点的有限值,树适合具有层次结构的数据
- 祖先 子孙 双亲 孩子 兄弟
- 结点的度 树的度
树中一个结点孩子的个数称之为该结点的度,树中结点的最大度数称之为树的度
- 分支结点【非终端结点】 叶子结点【终端结点】
度大于0 叫分支结点,度等于0叫叶子结点
- 深度 高度 层次
结点的层次从根开始定义,深度是从根节点自顶向下逐层累加,高度是从叶节点开始自底向上逐层累加
- 有序树 无序树
树中结点的各子树从左到右是有次序的,不能互换,称之为有序树,否则为无序树
- 路径和路径长度
路径是这两个结点之间所经过的结点序列构成,路径长度是指路径上所经过的边的个数
- 森林
森林是m(m>=0)棵互不相交的树的集合
4.2 二叉树的定义
二叉树是一种树形结构,子树有左右之分,次序不能任意颠倒,每个结点至多有两棵子树
- 特殊二叉树
- 满二叉树
- 完全二叉树
- 二叉排序树
- 平衡二叉树
4.3 二叉树的存储结构
4.3.1 顺序存储结构
二叉树的顺序存储是指用一组地址连续的存储单元依次从上到下,从左到右存储完全二叉树上的结点元素
- 完全二叉树
- 满二叉树
4.3.2 链式存储结构
由于顺序存储的空间利用率比较低,因此二叉树一般采用链式存储结构
- 二叉树的链式存储结构
1 | typedef struct BiTNode { |
4.4 二叉树的遍历
- 先序遍历<从上到下,从左到右>
若二叉树为空,什么也不做,否则,访问根节点,先序遍历左子树,先序遍历右子树
- 递归
1 | void PreOrder(BiTree T) { |
- 非递归
1 | void PreOrder2(BiTree T) { |
- 中序遍历<从左到右>
若二叉树为空,什么也不做,否则,中序遍历左子树,访问根节点,中序遍历右子树
- 递归
1 | void InOrder(BiTree T) { |
- 非递归
1 | void InOrder2(BiTree T) { |
- 后序遍历(从下到上,从左到右)
若二叉树为空,什么也不做,否则,后序遍历左子树,后序遍历右子树,访问根节点
- 递归
1 | void PostOrder(BiTree T) { |
- 非递归
1 | void PostOrder2(BiTree T) { |
- 层次遍历
1 | void levelOrder(BiTree T) { |
- 由遍历序列构造二叉树
由二叉树的先序序列和中序序列可以唯一确定一棵二叉树
由二叉树的后序序列和中序序列可以唯一确定一棵二叉树
由二叉树的层序序列和中序序列可以唯一确定一棵二叉树
只知道二叉树的先序序列和后序序列无法唯一确定一棵二叉树
4.5 线索二叉树
4.5.1 线索二叉树的定义
1 | typedef struct ThreadNode{ |
4.5.2 中序线索二叉树的构造
1 | void InTread(ThreadTree &p,ThreadTree &pre) { |
4.5.3 中序线索二叉树的遍历
- 中序线索二叉树中中序序列下的第一个结点
1 | ThreadNode * Firstnode(ThreadNode *p) { |
- 中序线索二叉树中结点p在中序序列下的后继
1 | ThreadNode *Nextnode(ThreadNode *p) { |
- 利用上面两个算法,可以写出不含头结点的中序线索二叉树的中序遍历的算法
1 | void Inorder(ThreadTree *T) { |
4.6 森林
4.6.1 树的存储结构
- 双亲表示法
1 |
|
- 孩子表示法
- 孩子兄弟表示法
1 | typedef struct CSNode{ |
4.6.2 树,森林,二叉树的转换
- 树转换为二叉树
<1> 在兄弟结点之间加一条连线
<2>对于每一个结点,只保留它与第一个孩子的连线,而与其他孩子的连线全部抹掉
<3>以树根为轴心,顺时针旋转45度
- 森林转换为二叉树
<1> 先将森林中每棵树转换为相应的二叉树
<2> 每棵树的根也可以视为兄弟关系,在每棵树的根之间加一根连线
<3> 以第一棵树的根为轴心顺时针旋转45度
4.6.3 树和森林的遍历
树的遍历
- 先根遍历
- 后根遍历
森林的遍历
- 先序遍历森林
- 中序遍历森林【后根遍历】
4.6.4 树与二叉树的应用
- 二叉排序树【BST】
- 非递归查找算法
1 | BSTNode *BST_Search(BiTree T,ElemType key) { |
- 二叉排序树的插入
1 | int BST_Insert(BiTree &T,KeyType k) { |
- 二叉排序树的构造
1 | void Creat_BST(BiTree &T,KeyType str[],int n) { |
平衡二叉树
哈夫曼树和哈夫曼编码
5. 图
图不可以是空图,图的顶点集V一定非空
5.1 图的属性
- 有向图
- 无向图
- 简单图、多重图
- 完全图【简单完全图】
- 子图
- 连通,连通图,连通分量
- 强连通图,强连通分量
- 生成树,生成森林
- 顶点的度,入度和出度
- 边的权和网
- 稠密图、稀疏图
- 路径,路径长度和回路
- 简单路径、简单回路
- 距离
- 有向树
5.2 图的存储和操作
5.2.1 邻接矩阵法
- 存储结构定义
1 |
|
5.2.2 邻接表法
- 存储结构定义
1 |
|
5.2.3 十字链表
5.2.4 邻接多重表
5.3 图的基本操作
1 | Adjacent(G,x,y)//判断图是否存在边<x,y>或(x,y) |
5.4 图的遍历
5.4.1 广度优先搜索【Breadth-First-Search,BFS】
类似树的层序遍历算法
- 伪代码
1 | bool visited[Max_Vertex_num];//访问标记数组 |
5.4.2 深度优先搜索【Depth-First-Search,DFS】
类似树的先序遍历
- 伪代码
1 | bool visited[Max_verTex_Num]; |
- 图的遍历与图的连通性
5.5 图的应用
最小生成树
- Prim算法
- Kruskal算法
最短路径
- Dijkstra算法求单源最短路径问题
- Floyd算法求各顶点之间最短路径问题
有向无环图描述表达式
拓扑排序
关键路径
6. 查找
在数据集合中寻找满足某种条件的数据元素的过程称为查找
6.1 查找的属性
- 查找表【查找结构】
用于查找的数据集合
- 静态查找表
- 关键字
- 平均查找长度
6.2 顺序查找
- 一般线性表的顺序查找
1 | typedef struct { |
- 有序表的顺序查找
- 折半查找【二分查找】
仅适用于有序的顺序表
1 | int Binary_Search(SeqList L,ElemType key) { |
- 分块查找【索引顺序查找】
6.3 B树和B+树
6.3.1 B树的属性
B树,多路平衡查找树
- B树的高度(磁盘存取次数)
- B树的查找
在B树中找结点,在结点内找关键词
- B树的插入
- B树的删除
6.3.2 B+树的基本概念
B+树是应数据库所需而出现的一种B树的变形树
一棵m阶的B+树需满足下列条件:
- 每个分支结点最多有m棵子树(孩子结点)
- 非叶根结点至少有两棵子树,其他每个分支结点至少有[m/2]棵子树
- 结点子树的个数与关键字个数相等
- 所有叶节点包含全部关键字及指向相应记录的指针,叶结点中将关键字按大小顺序排列,并且相邻叶结点按大小顺序相互链接起来
- 所有分支结点(可视为索引的索引)中仅包含它的各个子结点(即下一级的索引块)中关键字的最大值及指向其子结点的指针
6.4 散列表
根据关键字而直接进行访问的数据结构
6.4.1 散列表的构造方法
- 直接定址法
H(key) = key 或H(key) = a*key + b
- 除留余数法
H(key) = key % p
- 数字分析法
- 平方取中法
6.4.2 处理冲突的方法
开放定址法
- 线性探测法
- 平方探测法
- 再散列法
- 伪随机序列法
拉链法【链接法,chaining】
7. 排序
就是重新排列表中的元素,通常由比较和移动,分为插入排序、交换排序、选择排序、归并排序、基数排序,大部分排序算法仅适用于顺序存储的线性表
7.1 插入排序
- 普通插入排序
1 | void InsertSort(ElemType A[],int n) { |
- 折半插入排序
稳定的排序方法
1 | void InsertSort(ElemType A[],int n) { |
- 希尔排序【缩小增量排序】
1 | void ShellSort(ElemType A[], int n) { |
7.2 交换排序
- 冒泡排序
1 | void BubbleSort(ElemType A[],int n) { |
- 快速排序
1 | void QuickSort(ElemType A[],int low,int high) { |
7.3 选择排序
- 简单选择排序
1 | void SelectSort(ElemType A[],int n) { |
- 堆排序
- 建立大根堆
1 | void BuildMaxHeap(ElemType A[], int len) { |
- 堆排序算法
1 | void HeapSort(ElemType A[], int len) { |
堆排序是一种不稳定的排序方法
7.4 归并排序和基数排序
- 归并排序
1 | ElemType *B = (ElemType *)malloc((n+1)*sizeof(ElemType)); |
上面的代码中,只有一个while会执行
- 合并
1 | void MergSort(ElemType A[],int low,int high) { |
7.5 各种内部排序算法的比较
序号 | 算法种类 | 时间复杂度最好情况 | 时间复杂度平均情况 | 时间复杂度最坏情况 | 空间复杂度 | 是否稳定 |
---|---|---|---|---|---|---|
1 | 直接插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 是 |
2 | 冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 是 |
3 | 简单选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 否 |
4 | 希尔排序 | O(1) | 否 | |||
5 | 快速排序 | O(nlog2n) | O(nlog2n) | O(n^2) | O(nlog2n) | 否 |
6 | 堆排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(1) | 否 |
7 | 2路归并排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(n) | 是 |
8 | 基数排序 | O(d(n+r)) | O(d(n+r)) | O(d(n+r)) | O(r) | 是 |
7.6 外部排序
外部排序是指待排序文件较大,内存一次放不下,需存放在外存的文件的排序,通常采用归并排序法
- 外部排序的总时间 = 内部排序的所需时间 + 外存信息读写的时间 + 内部归并所需的时间
- 多路平衡归并与败者树
- 置换选择排序
- 最佳归并树
8. 总结
这次对于数据结构有了基本的了解,自己不知道的东西还是有许多的。