文章目录
- 堆和二叉树
- 一、定义与性质
- 二、结构特点
- 三、应用场景
- 四、查找效率
- 解释
- 荷兰围棋问题
- 拓扑排序的树
- 逆拓扑排序
- 邻接表的存储
- 二叉树、二叉平衡树、图刷题笔记
堆和二叉排序树是数据结构中两种不同的树状结构,它们之间存在显著的区别。以下是对这两种数据结构的详细比较:
堆和二叉树
一、定义与性质
-
堆
- 堆是一种特殊的完全二叉树。
- 在堆中,每个节点的值都满足特定的顺序关系。具体分为:
- 大根堆:任何一个父节点的值都大于或等于它的子节点的值。
- 小根堆:任何一个父节点的值都小于或等于它的子节点的值。
- 堆通常用于实现排序算法,如堆排序。
-
二叉排序树(二叉查找树、二叉搜索树)
- 二叉排序树是一种具有特定性质的二叉树。
- 在二叉排序树中,左子树上所有节点的值均小于根节点的值,右子树上所有节点的值均大于根节点的值。
- 左右子树也分别为二叉排序树。
- 二叉排序树主要用于实现动态查找操作。
二、结构特点
-
堆
- 堆是完全二叉树,因此具有完全二叉树的性质,如节点按层序排列,且除最后一层外,每一层都是满的,最后一层的节点都靠左对齐。
- 堆中节点的值满足特定的顺序关系,但左右子节点之间的大小关系没有限定。
-
二叉排序树
- 二叉排序树的形状取决于插入节点的顺序,因此不一定是完全二叉树。
- 在二叉排序树中,每个节点的左子节点的值小于该节点的值,右子节点的值大于该节点的值,这保证了中序遍历二叉排序树时可以得到一个有序序列。
三、应用场景
-
堆
- 堆主要用于实现排序算法,如堆排序。
- 堆还可以用于实现优先级队列等数据结构。
-
二叉排序树
- 二叉排序树主要用于实现动态查找操作,如插入、删除和查找节点。
- 由于二叉排序树的中序遍历可以得到有序序列,因此也可以用于排序操作,但通常不是其主要应用场景。
四、查找效率
-
堆
- 在堆中查找一个节点需要进行遍历,其平均时间复杂度是O(n)。
- 但由于堆的特殊性质,我们可以快速地找到堆顶元素(大根堆中的最大值或小根堆中的最小值),其时间复杂度为O(1)。
-
二叉排序树
- 在二叉排序树中查找一个节点的平均时间复杂度是O(log n)(在平衡二叉排序树中)。
- 但如果二叉排序树退化为链表(如按序插入节点),则查找效率会下降到O(n)。
同时满足大根堆和二叉排序树:没有右子树
同时满足小根堆和二叉排序树:没有左子树
如果只想得到一个序列中第k(k>=5)个最小元素之前的部分排序序列,最好采用什么排序方法?
冒泡排序、堆排序和简单选择排序。
k超过某一个值之后堆排序是最好的。
已知线性表按顺序存储,且每个元素都是不相同的整数型元素,设计把所有奇数移动到所有偶数前边的算法(要求时间最少,辅助空间最少)。
可以利用快速排序中的分区(partition)思想。快速排序的分区步骤能够在O(n)时间复杂度内将一个数组分成两部分,这里我们可以利用这个特性将奇数移到数组的前面,偶数移到数组的后面。
该算法的时间复杂度是 O(n),其中 n 是数组的长度。
这是因为算法使用了双指针技术,并且每个元素最多只被访问一次。具体来说:
left
指针从数组的开头向右移动,直到它遇到一个偶数或者与right
指针相遇。right
指针从数组的末尾向左移动,直到它遇到一个奇数或者与left
指针相遇。- 在每次循环中,
left
和right
指针最多各移动一步,直到它们相遇或交错。
由于数组中的每个元素最多只会被 left
或 right
指针访问一次,因此总的时间复杂度是线性的,即 O(n)。
以下是一个使用C++实现的算法,它利用了双指针和快速排序中的分区思想,但整体时间复杂度是O(n):
#include <iostream>
#include <vector>// 函数声明
void moveOddToFront(vector<int>& nums);int main() {vector<int> nums = {12, 34, 45, 9, 8, 90, 7};cout << "Original array: ";for (int num : nums) {cout << num << " ";}cout << endl;moveOddToFront(nums);cout << "Modified array: ";for (int num : nums) {cout << num << " ";}cout << endl;return 0;
}void moveOddToFront(vector<int>& nums) {int left = 0;int right = nums.size() - 1;while (left < right) {// 从左向右找到第一个偶数while (left < right && nums[left] % 2 != 0) {left++;}// 从右向左找到第一个奇数while (left < right && nums[right] % 2 == 0) {right--;}// 交换找到的偶数和奇数if (left < right) {std::swap(nums[left], nums[right]);left++;right--;}}
}
解释
-
初始化指针:
left
指针从数组的开头开始。right
指针从数组的末尾开始。
-
双指针遍历:
- 使用
while
循环,当left
小于right
时继续。 - 从左向右遍历,找到第一个偶数(
nums[left] % 2 == 0
)。 - 从右向左遍历,找到第一个奇数(
nums[right] % 2 != 0
)。
- 使用
-
交换元素:
- 如果找到了这样的偶数和奇数,就交换它们的位置。
- 然后移动
left
和right
指针,继续寻找下一个需要交换的元素。
-
终止条件:
- 当
left
和right
相遇或交错时,遍历结束,所有奇数都已经被移动到偶数前面。
- 当
这个算法的时间复杂度是O(n),因为我们只遍历了数组一次。辅助空间复杂度是O(1),因为我们只使用了几个额外的变量来存储指针和临时值。
荷兰围棋问题
荷兰国旗问题:设有一个仅由红、白、蓝三种颜色的条块组成的条块序列,存储在-个顺序表中,请编写一个时间复杂度为 O ( n ) O(n) O(n)的算法,使得这些条块按红、白、蓝 的顺序排好,即排成荷兰国旗图案。请完成算法实现:
typedef enum{RED,WHITE,BLUE} color;//设置枚举数组
void Flag Arrange(color a[]int n){...}
算法思想
顺序扫描线性表,将红色条块交换到线性表的最前面,蓝色条块交换到线性表的最后面。为此,设立三个指针,其中j工作指针,表示当前扫描的元素,i以前的元素全部为红色,k以后的元素全部为蓝色。根据j所指示元素的颜色,决定将其交换到序列的前部或尾部。初始时i=0,k=n-1。
j指针它扫描到红色块那么就是跟i指针所指的颜色块交换并且ij指针它同时要++
如果j扫描的是蓝色块那么我们就跟这个k指针所指的颜色块交换并且我k指针要减减也就k指针要前移
如果说j扫描的是白色块我不需要做任何的交换,直接就是j指针往前移动
typedef enum{RED,WHITE:,BLUE} color;
//设置枚举数组
void Flag Arrange(color a[],int n)
{
int i=0,j=0,k=n-1;
while(j<=k)
switch(a[j]){//判断条块的颜色
case RED:Swap(a[i],a[j]);i++;j++;break;
//红色,则和 i 交换
case WHITE:j++;break;
case B:Swap(a[j],a[k]);k--
//蓝色,则和k交换
//这里没有 1++语句以防止交换后 a[j]仍为蓝色的情况
}
}
顺序表交换元素效率更高
关键路径:从启动项目到完成该项目,时间开销至少需要…多少。
有向无环图,一定可以转化为一个上三角或下三角矩阵。但是需要调整顶点的编号。
如果要用上三角矩阵表示有向无环图的邻接矩阵,可以对图进行拓扑排序,按照拓扑排序序列,重新调整各个顶点的编号。这样可以确保,所有的弧都是从小编号顶点指向大编号顶点,从而也就保证了邻接矩阵可以转化为“上三角矩阵”
拓扑排序的树
1、拓扑排序和逆拓扑排序序列都可能不唯一;
2、若图中有环,则不存在拓扑排序序列或者逆拓扑排序序列;
逆拓扑排序
S t e p 1 Step 1 Step1:把各个操作数不重复地排成一排。
S t e p 2 S t e p 2 Step2:标出各个运算符的生效顺序(同级别之间任意)。
S t e p 3 S t e p 3 Step3:按顺序加入运算符,注意“分层”。(若某个运算符的执行要基于另一个运算符和操作数的执行结果来进行,则前一个运算符在后一个的“上一层”)
S t e p 4 S t e p 4 Step4:从底层向上逐层检查,看同层的运算符是否可以“合体”。
注意:在表达式的有向无环图表示中,不可能出现重复的操作数顶点
邻接表的存储
参考链接
#define MaxVertexNum 1007
typedef struct ArcNode {int adjvex;struct ArcNode* nextarc;
}ArcNode, * ArcList;
typedef struct VNode {int data;ArcNode* firstarc;
}VNode, AdjList[MaxVertexNum];
typedef struct {AdjList vertices;int vexnum, arcnum;
}ALGraph;
预定义、预处理和基本操作函数如下
int indegree[MaxVertexNum], print[MaxVertexNum];
//记录各点入度,记录拓扑排序序列inline void Init(ALGraph& G) {//预处理for (int i = 1; i <= G.vexnum; ++i) {ArcList h = (ArcList)malloc(sizeof(ArcList));h->adjvex = 0;h->nextarc = NULL;G.vertices[i].data = 0;G.vertices[i].firstarc = h;}memset(indegree, 0, sizeof(indegree));memset(print, 0, sizeof(print));return;
}inline void AG_Insert(ALGraph& G, int i, int j) {//头插法加入边ArcNode* p = (ArcNode*)malloc(sizeof(ArcNode));p->adjvex = j;ArcNode* head = G.vertices[i].firstarc;ArcNode* tail = G.vertices[i].firstarc->nextarc;p->nextarc = tail;head->nextarc = p;return;
}
拓扑排序的实现。【静态数组模拟栈的方法】
inline bool Topologicalsort(ALGraph G) {int Sta[MaxVertexNum], top = 0, count = 0;//用静态数组模拟栈,top为栈顶指针,count为拓扑序列数组下表memset(Sta, 0, sizeof(Sta));int i;for (i = 1; i <= G.vexnum; ++i)if (!indegree[i])Sta[++top] = i;//将初始入度为0的顶点进栈while (top) {//当栈不空时i = Sta[top--];//栈顶元素出栈print[++count] = i;//输出顶点ifor (ArcNode* p = G.vertices[i].firstarc; p != NULL; p = p->nextarc) {//将所有i指向的顶点入度减1,并将入度减为0的顶点压入栈Sta中int v = p->adjvex;if (!v)continue;if (!(--indegree[v]))Sta[++top] = v;//度为0则入栈p->adjvex = 0;}}if (count < G.vexnum)return false;//排序失败,图中存在回路else return true;//拓扑排序成功
}
DFS实现拓扑排序
对于一个有向无环图G,其任意结点u,v,它们之间的关系必然满足下列三种之一。
- 若u是v的祖先,则在调用DFS访问u之前,必然已经对v进行过DFS访问,即v的DFS访问顺序先于u。从而可考虑在DFS函数中设置一个时间标记,在DFS调用结束时,对各顶点即使。因此,祖先的结束时间必然大于子孙的结束时间。
- 若u是v的子孙,则v为u的祖先,按1中的思路,v的结束时间大于u的结束时间。
- 若u和v没有路径关系,则u和v在拓扑序列的关系任意。
于是按结束时间从大到小排列,就可以得到一个拓扑排序序列。
int tim, finishtime[MaxVertexNum];
bool visited[MaxVertexNum];//访问标记数组inline void DFSTravere(ALGraph G) {memset(visited, false, sizeof(visited));//初始化memset(finishtime, 0, sizeof(finishtime));tim = 0;for (int i = 1; i <= G.vexnum; ++i)//从第一个顶点开始深搜if (!visited[i])DFS(G, i);return;
}inline void DFS(ALGraph G, int v) {visited[v] = true;for (ArcNode* p = G.vertices[v].firstarc->nextarc; p != NULL; p = p->nextarc) {int w = p->adjvex;//依次遍历当前顶点的邻边未访问的顶点if (!visited[w])DFS(G, w);}finishtime[v] = ++tim;//搜索深度越深,tim值越小//如果要输出逆拓扑排序序列,只需把这一行改为输出v即可return;
}
采用邻接表存储时,拓扑排序的时间复杂度为 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(∣V∣+∣E∣),采用邻接矩阵存储时,拓扑排序的时间复杂度为 O ( ∣ V ∣ 2 ) O(∣V∣^2) O(∣V∣2)
- 入度为0的顶点,即没有前驱活动的或前驱活动都已经完成的顶点,工程可以从这个顶点所代表的活动开始或继续。
- 若一个顶点有多个直接后继,则拓扑排序的结果通常不唯一;但若各个顶点已经排在一个线性有序的序列中,每个顶点有唯一的前驱后继关系,则拓扑排序的结果是唯一的。
- 由于AOV网中各顶点的地位平等,每个顶点的编号是人为的,因此可以按拓扑排序的结果重新编号,生成的AOV网的新的邻接存储矩阵,这种邻接矩阵可以是三角矩阵;但对一般的图来说,若其邻接矩阵是三角矩阵,则存在拓扑序列;反之则不一定成立。
逆拓扑实现方法一
#define Maxsize 100
//拓扑排序
int Dedegree[Maxsize]; //存储当前顶点的出度
int print[Maxsize]; //记录拓扑序列
Stack<int> S; //存储出度为0的顶点bool TopologicalSort(Graph G)
{InitStack(S); //初始化栈for(int i=0;i<G.vexnum;i++){if(Dedegree[i]==0) //度为0的顶点入栈 {push(S,i);}} int count=0; //计数while(!IsEmpty(S)){pop(S,i);print[count++]=i;for(p=G.vertices[i].firstarc;p;p=p->nextarc)//将指向i的顶点的出度减1,并且将出度减为0的顶点压入栈中 {v=p->adjvex;if(!(--Dedegree[v])){push(S,v)}}if(count<G.vexnum) //拓扑排序失败,说明有回路 {return false;}else{return true;} } }
逆拓扑实现方法二
//DFS实现的逆拓扑排序bool visit[Maxnum]; //标记数组,防止顶点被多次访问 void DFSTraverse(Graph G){for(int i=0;i<G.vexnum;i++){visit[i]=false;}for(int i=0;i<G.vexnum;i++){if(!visit[i])DFS(G,i);}} void DFS(Graph G,int v){visit[v];visit[v]=true;for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,V,W)) //从顶点v出发,深度优先遍历G{if(!visit[w]);DFS(G,w);} cout<<v; //输出顶点 }
二叉树、二叉平衡树、图刷题笔记
三叉链表表示二叉树的空指针:n+2
二叉链表表示二叉树的空指针:n+1
三叉树的最小高度使用等比数列求和得到结果。
二叉树采用二叉链表,指针域有2n个,n个节点有n-1个指针,空指针2n-(n-1)=n+1
三叉树采用三叉链表,指针域有3n个,n个节点有n-1个指针,空指针3n-(n-1)=2n+1
四叉树采用四叉链表,指针域有4n个,n个节点有n-1个指针,空指针4n-(n-1)=3n+1
k叉树采用k叉链表,指针域有kn个,n个节点有n-1个指针,空指针kn-(n-1)=(k-1)n+1
二叉树采用顺序存储使用满二叉树的存储方法,因为需要随机访问,所以即使不保存数据的地方也是需要有的(满的访问方式)。
重要结论
除根结点外,其他每个结点都是某个结点的孩子,因此树中所有结点的度数加1等于结点数,也即所有结点的度数之和等于总结点数减1。这是一个重要的结论,做题时经常用到。
树的路径长度是指树根到每个结点的路径长的总和,根到每个结点的路径长度的最大值应是树的高度减1。注意与哈夫曼树的带权路径长度相区别。
树中结点数比边数多1
高度为h的满二叉树所含的树的个数一定是h
兄弟孩子表示法:左指针域为空 ,孩子结点个数
- 连通图可能是树,可能存在环
有回路未必是连通的
若一个无向图有n个顶点和n-1条边,可以使它连通但没有环(即生成树),但若再加一条边,在不考虑重边的情形下,则必然会构成环。
- 强连通图是有向图,一定存在环
对无向连通图做一次深度优先搜索,可以访问到该连通图的所有顶点,B正确:有回路的无向图不一定是连通图,因为回路不一定包含图的所有结点,
若 E’中的边对应的顶点不是V’的元素,V’和{E’)无法构成图
无向图的极大连通子图称为连通分量,
图的遍历要求每个结点只能被访问一次,且若图非连通,则从某一顶点出发无法访问到其他全部顶点
n个顶点,连通无向图边数至少为n-1,强连通有向图至少为n
强连通图:在图论中,如果一个有向图(Directed Graph)的每对顶点之间都存在双向可达的路径,即对于图中的任意两个顶点u和v,都存在从u到v的路径以及从v到u的路径,那么这个图被称为强连通图。一个有向图是强连通的,当且仅当图中存在一个回路,这个回路至少包含图中的每个节点一次。
强连通分量是【极大强连通子图】,任意两个顶点之间有方向相反的两条路径。由定义不难得出,若一个顶点只有出边或入边,则该顶点必定单独构成一个连通分量。
图中,顶点B只有出边,其他所有顶点都不可能有到顶点 B的路径,所以顶点 B单独构成一个强连通分量。在【顶点A、C、D、E中】,任意两个顶点之间都有方向相反的两条路径,所以可构成一个强连通分量。
连通图:连通图是指在一个无向图(Undirected Graph)中,任意两个顶点之间都存在至少一条路径使它们相连。这里的关键是“无向”和“至少一条路径”。连通性并不要求路径是双向的,也就是说,从一个顶点到另一个顶点可能存在路径,但从后者到前者不一定存在直接路径(尽管通过其他顶点可能仍然连通)。
一对,度为2*(n-1)
二叉排序树,不需要调整其为平衡:
二叉平衡树(AVL)需要记得的例子
二叉平衡树被删除叶子结点之后重新插入也有可能失衡调整
二叉平衡树被删除非叶子结点之后重新插入可能与被删除结点之前一样
注意二叉平衡树和二叉排序树的区别
平衡二叉树最少结点的公式: