数据结构复习
算法复杂度
- 若两端算法分别由复杂度和则
- 若是关于 n 的 k 阶多项式,那么
- 一个for循环的时间复杂度等于循环次数乘以循环体代码的复杂度
例题1 给定N个整数序列{A1,A2,……,An};求函数的最大值
1 | //算法1 时间复杂度O(n^2) |
1 | //算法2 分而治之(递归) 时间复杂度O(nlogn) 见归并排序 |
1 | //算法3 在线处理(动态规划) 时间复杂度O(n) |
(考)线性表(查找,增加,删除)
定义:(对象集)一个线性表是n
个数据元素的有限序列。由同类型元素构成有序序列的线性结构。
- 线性表**顺序存储,**抽象表示(考)
1 | typedef struct LNode *List; |
- 链式存储,抽象表示
1 | typedef struct LNode *List; |
1 | //单链表实现 |
(考)堆栈
- 插入数据:入栈(Push)
- 删除数据:出栈(Pop)
- 后入先出:Last In First Out(LIFO)
1 | //!!!顺序存储实现,抽象 |
- 使用一个数组实现两个堆栈,要求最大地利用数组空间,使数组只要有空间入栈操作就可以成功。(使这两个栈分别从数组的两头开始向中间生长,当两个栈的栈顶指针相遇时,表示两个栈都满了)
1 |
|
- 表达式求值(中缀转后缀)
- 运算数:直接输出;
- 左括号:压入堆栈;
- 右括号:将栈顶的运算符弹出并输出,直到遇到左括号(出栈,不输出);
- 运算符:
- 若优先级大于栈顶运算符时,把它进行压栈;
- 若优先级小于等于栈顶运算符时,将栈顶运算符弹出并输出;再比较新的栈顶运算符,直到该运算符大于栈顶运算符优先级为止,然后将该运算符压栈;
- 若各对象处理完毕,则把堆栈中留存的运算符一并输出。
1 | // |
队列
- 插入数据:入队列(AddQ)
- 删除数据:出队列(DeleteQ)
- 先来先服务
- 先进先出:FIFO
1 |
|
树和二叉树
顺序查找
1 | struct LNode{ |
####二分查找
- 假设
n
个数据元素的关键字满足有序(比如:小到大)(k1<k2<····<kn)并且是连续存放**(数组)**,那么可以进行二分查找。 - 二叉排序树->动态查找问题
1 | struct LNode{ |
树
定义:n(n>=0)个结点的有限集合,n=0时,称为空树,这是一种情况。在任意一科非空树中应满足:
- 有且仅有一个特定的称为根的结点。
- 其余结点可分为m(m>0)个互不相交的有限集{T1,T2,…,Tm},其中每个集合本身又是一棵树,称为原来树的“子树”。
树与非树?
- 子树是不相交的;
- 出了根结点外,每个结点有且仅有一个父结点;
- 一棵N个结点的树有N-1条边。
一些基本定义:
- 结点的度:结点的子树个数;
- 树的度:树的所有结点中最大的度数;
- 叶结点: 度为0的结点;
- **父结点:**有子树的结点是其子树的根结点的父结点;
- **子结点:**子结点也称孩子结点;
- **兄弟结点:**具有同一父结点的各结点彼此是兄弟结点。
- 路径和路径长度:从n1->nk的路径为一个结点序列,路径所包含边的个数为路径长度。
- 祖先结点:沿树根到某一结点路径上所有结点都是这个结点的祖先结点。
- 子孙结点:某结点的子树中所有结点都是这个结点的子孙结点。
- **结点的层次:**根节点为1层,向下累加。
- 数的深度:树中所有结点最大层次是这棵树的深度。
基本性质:
- 树中的结点数等于所有结点的度+1;
- 度为m的树中第i层上至多有个结点(i>=1);
- 高度为h的m叉树至多有(-1)/(m-1)个结点。
- 具有n个结点的m叉树的最小高度为┌┐
二叉树
1 | //儿子兄弟表示法 |
- 二叉树有左右之分,5中形态(空,根,根左,根左右,根右),其中特殊的二叉树有,满二叉树,完全二叉树(不满但是结点序号和顺序必须一致),二叉排序树(左小右大,二分查找),平衡二叉树(任一结点左子树和右子树深度之差不超过1)。
操作集:
- Boolean IsEmpty(struct tree *BT):判别BT是否为空;
- Void Traversal(struct tree *BT):遍历,按某个顺序访问每个结点;
- void PreOrderTraversal(struct tree *BT):先序遍历— 根,左,右;
- void InOrderTraversal(struct tree *BT):中序遍历—左,根,右;
- void PostOrderTraversal(struct tree *BT):后序遍历—左,右,根;
- void LevelOrderTraversal(struct tree *BT):层次遍历,从上到下,从左到右
- struct tree CreatBinTree():创建一个二叉树。
1 | //链表存储 |
(考)二叉树遍历
1 | //先序遍历 |
二叉树遍历应用:
1 | //输出二叉树中叶子结点,先序遍历 |
二元运算表达式树及其遍历
通过两种序列(必须有中序遍历)就可以确定一个唯一的二叉树
1 | //二叉树静态链表 |
(考)二叉搜索树/查找树/排序树
1 | Position Find(ElementType X,struct tree *BST);//从二叉排序树BST中查找元素X,返回其所在结点的地址; |
插入操作
1 | struct tree* Insert(ElementType k,struct tree *BST){ |
删除操作
- 当为叶子结点直接删除
- 当删除结点只有一个儿子,用儿子代替
- 当左右都有儿子结点都有二子结点时
- 右子树最小元素代替
- 左子树的最大元素
1 | //若为叶子结点直接删除 |
1 | //代码实现 |
平衡二叉树
- 任一结点左子树和右子树深度之差不超过1
- 搜索树结点不同插入次序,将导致不同的深度和平均查找长度ASL
- 斐波那契数列(1,1,2,3,5,…),nh=n(h-1)+n(h-2)+1 =》给定结点数为n的平衡二叉树(AVL)最大高度为h=O(log2n).
调整:
- RR旋转
- LL旋转
- LR旋转
(考)线索二叉树
线索化
- 若无左子树,则将左指针指向其前驱结点;
- 若无右子树,则将右指针指向其后继结点。
前驱结点
- 若左指针为线索,则其指向结点为前驱结点;
- 若左指针为左孩子,则其左子树的最右侧结点为前驱结点。
后继结点
- 若右指针为线索,则其指向结点为后继结点;
- 若右指针为右孩子,则其右子树的最左侧结点为后继结点。
中序线索二叉树,将中序序列中对应的结点结构中空指针设置前驱和后继,并设置头节点,指向根和尾结点,而左子树最后一个元素的前驱和右子树的尾结点元素的后继指向头节点。称为线索链表。
1 |
|
完全二叉搜索树
完全二叉搜索树=二叉搜索树+完全二叉树
特点:不浪费空间,使用数组更加方便,使用层序遍历==直接输出
算法思想:首先从小到大排序,找根节点,分为左右子树,再递归将左右子树分别填入
####堆
**优先队列:特殊的“队列”,取出元素的顺序是依照元素的优先权(关键字)**大小,而不是元素进入队列的先后顺序。
基本结构体及构建
1 | struct HeapStruct{ |
堆的插入
1 | /*保证插入后仍然是完全二叉树,且每个结点都是其子树所有结点的最大值 |
堆的删除
1 | /*删除最大元素或某个元素用最后一个元素代替当前位置, |
- 如何建立一个堆?
- 一个一个的插入(效率低)
- 在线性时间复杂度下建立最大堆。
- 将N个元素按输入顺序存入,先满足完全二叉树的结构性;
- 调整各结点位置,以满足最大堆的有序特性。
- 思路:从倒数第一个有儿子的父结点(size/2)开始一个一个调整(删除结点的思路)为堆
1 | //调整为最大堆 |
哈夫曼树和编码
#####(考)哈夫曼树
定义:带权路径长度(WPL):设二叉树有n个叶子结点,每个叶子结点带有权值,从根结点到每个叶子结点的长度为,则每个叶子结点的带权路径长度之和就是:
最优二叉树或哈夫曼树:WPL最小的二叉树。
哈夫曼树构造:
- 由一段有序序列(可以用堆实现,效率更高O(NlogN))中挑选最小的两个合并为一棵树后将权值重新加入排序,重复上述过程直到创建树成功.
哈夫曼树特点:
- 没有度为1的结点
- n个叶子结点的哈夫曼树共有2n-1个结点;
- 哈夫曼树的任意非叶子结点的左右子树交换后仍是哈夫曼树;
- 对同一组权值**{},存在不同构的哈夫曼树,但WPL**值相同。
1 | //代码实现 |
#####哈夫曼编码
Q:给定一段字符串,如何对字符进行编码,可以使该字符串的编码存储空间最少?
- 按照频率给定权值
Q:如何避免二义性?
- 前缀码:任何字符的编码都不是另一字符编码的前缀
用二叉树进行编码:
- 左右分支:0、1
- 字符只在叶结点上
###图
- 表示多对多的关系
- 包含
- 一组顶点:通常用 V(Vertex) 表示顶点集合
- 一组边:通常用 E(Edge) 表示边的集合
- 边是顶点对:(v,w) ∈ E,其中 v,w ∈ V
- 有向边 <v,w> 表示从 v 指向 w 的边(单行线)
- 不考虑重边和自回路
图的表示法
邻接矩阵法
Q:对于无向图的存储,怎样可以省一半的空间?
- 用一个长度为N/(N+1)/2 的1 维数组 A 存储{},则在A中对应的下标为:
- 对于网络,只要把的值定义为边<>的权重即可。
对于稀疏图浪费空间(稠密图还是很合算),也浪费时间。
邻接表
- 方便找任一顶点的所有“邻接点”
- 节约稀疏图的空间
- 需要N个头指针 + 2E 个结点(每个结点至少2个域)
- 只方便计算无向图的度,不太适合有向图
图的遍历
(考)DFS 深度优先搜索
1 | //递归访问,抽象表示,类似于树的先序遍历,栈也可 |
- 采用队列实现
若有N个顶点、E条边,时间复杂度是
- 用邻接表存储图,有O(N+E)
- 用临界矩阵存储图,有O(N^2)
(考)BFS 广度优先搜索
1 | //类似层序遍历队列实现,每次出队时入队相邻未访问元素 |
若有N个顶点、E条边,时间复杂度是
- 用邻接表存储图,有O(N+E)
- 用临界矩阵存储图,有O(N^2)
每调用一次BFS/BFS 就是把 V 所在的连通分量遍历了一遍。
####如何建立一个图
1 | //邻接矩阵 |
单元最短路径
(考)Dijkstra
1 | template<class Type> |
最小生成树
定义:
- 是一棵树
- 无回路
V
个顶点一定有V-1
条边
- 是生成树
- 包含全部顶点
V-1
条边都在图里
- 边的权重和最小
等价于图连通
贪心算法:
- 贪:每一步要求最好
- 好:权重最小的边
- 需要约束:
- 只能用途中有的边
- 只能正好用掉
V-1
条边 - 不能有回路
(考)Prim算法
算法思想:
从连通网 N={V,E}中的某一顶点U0出发,选择与它关联的具有最小权值的边(U0,v),将其顶点加入到生成树的顶点集合U中。以后每一步从集合U中的所有顶点边中选择一条最小权值边(u,v)同时此边(u,v)所连接的顶点不包含在集合U中的任何顶点,则把它的顶点加入到集合U中。如此继续下去,直到网中的所有顶点都加入到生成树顶点集合U中为止。
1 | //时间复杂度O(|V|2)适合稠密图 |
Kruskal算法
算法思想:
把森林合并成树
1 | //时间复杂度(使用最小堆放权重集合 并查集检查回路)O(|E|log|E|) 适合稀疏图 |
(考)查找和内部排序
1 |
|
简单排序
冒泡排序
算法思想:比较相邻两个元素大小,若从小到大排则将大元素始终置后,每一趟完成会将最大的元素放到确定的位置。
1 | void Bubble_Sort(ElementType A[],int N){ |
插入排序
算法思想:从前往后取数,并从取数位置从后向前比较,将每一个数向后移,插入到合适得位置,从第一个数开始,第零个不动。
1 | void Insertion_Sort(ElementType A[],int N){ |
希尔排序
算法思想:通过设定增量序列,实现插入排序
1 | void Shell_sort(ElementType A[],int N){ |
选择排序
1 | void Selection_Sort(ElementType A[],int N){ |
堆排序
1 | //算法1 时间O(NlogN) 并且需要额外O(N)空间,并且复制元素需要时间 |
归并排序
核心:两个有序子列得归并
思想:比较两个序列中元素得大小,挑出小元素放到第三个序列中,适合外部排序
递归方式
1 | //时间 O(N) 两个子列一共有N个元素 |
过程图解:解决一段导回去一段
不合算,重复malloc
非递归方式
算法思想:利用临时数组,两边倒
1 | void Merge1(ElementType A[],ElementType TmpA[],int L,int R,int RightEnd){ |
快速排序
算法思想:分而治之(递归),首先随机找一个数作为主元,将集合分为以主元为标识得左右两集合,通过递归将两个集合排序,最后合并三部分(左集合,主元,右集合)。
1 | //最好情况,每次主元中分两个数组O(nlogn) 最坏O(N^2) |
1 | void Quicksort(ElementType A[],int Left,int Right){ |
表排序
算法思想:间接排序,定义一个指针数组(数组下标/指针)作为“表”(table)
- N个数字得排列一定是由若干个独立得环组成
- 每完成一个移动就重置
table
等于自己,最后一个环放置第一个拿出去的临时数
- 每完成一个移动就重置
基数排序
算法思想:采用LSD”次位优先“, 首先根据个位数的数值,在走访数值时将它们分配至编号0到9的桶子中, 接下来将这些桶子中的数值重新串接起来 , 接着再进行一次分配,这次是根据十位数来分配 , 接下来将这些桶子中的数值重新串接起来 , 这时候整个数列已经排序完毕;如果排序的对象有三位数以上,则持续进行以上的动作直至最高位数为止。