数据结构总结
1. 简介
数据结构三要素:数据的逻辑结构、数据的存储结构和数据操作。
- 数据逻辑结构。例如:集合、线性结构、树结构、图结构等。
- 数据存储结构。
1). 顺序存储结构:用一组连续的存储单元来存放数据元素,逻辑上相邻的元素在物理上也是相邻的。
2). 链式存储结构:数据元素的存放位置由编译器随机分配,节点之间的逻辑关系通过指针表示。
3). 索引存储结构:在存储结点的同时增加一个索引表,索引表中的每一项称为一个索引项,索引项包含一个结点的关键码和结点的存储位置。
4). 散列存储结构: - 数据的操作。数据元素在具体存储结构下的实现算法。
2. 线性表
线性表分为顺序表和链表两种。顺序表是用一组地址连续的存储单位依次存储线性表中的各元素,通过位置来表示数据元素之间的线性逻辑关系;链表是用一组任意的存储单位存储线性表中的各位置,通过指针来表示数据元素之间的线性逻辑关系。
2.1 顺序表
顺序表结构的定义:
typedef int DataType;
struct List{
int Max; //最大元素个数
int n;//实际元素个数
DataType *elem;//首地址
};
2.2 链表
链表结构的定义:
typedef int DataType
struct Node{
DataType data;
struct Node* next;
}LinkNode;
void InitList(LinkNode * &L) {
L = (LinkNode *)malloc(sizeof(LinkNode));
L -> next = NULL;
}
3. 栈和队列
3.1 栈结构
规则:后进先出,栈顶进栈顶出。
应用
-
递归
斐波那契数列数列:
int Fib(int n){ if(n == 0){ return 0; //边界条件 }else if(n == 1){ return 1; //边界条件 }else{ return Fib(n-1) + Fib(n-2); //递归表达式 } }
在递归调用的过程中,系统为每一层的返回点、局部变量、传入实参等开辟了递归工作栈来进行数据存储,递归次数过多容易造成栈溢出等。而其效率不高的原因是递归调用过程中包含很多重复的计算。下面以n=5为例,列出递归调用执行过程,如图所示:
-
进制转换
在进制转换中,最先计算得到的余数结果的最低位,最后计算得到的玉树是结果的最高位,即结果的顺序是计算顺序的逆序,正好符合栈后进先出的特点。例:十进制转八进制 void conversion(Stack s,int n){ while(n){ s.push(n%8); n=n/8; } while(!s.empty()){ printf("%d",s.top()); s.pop(); } }
-
括号匹配
若是左括号则进栈,若是右括号则弹出栈顶元素,判断与当前元素是否匹配,若是不匹配则可判定为此括号串不匹配。class Solution { public: bool isValid(string s) { stack<char> v; for (int i = 0; i < s.size(); i++) { //左括号进栈 if (s[i] == '(' || s[i] == '[' || s[i] == '{') { v.push(s[i]); } else if (s[i] == ')' || s[i] == ']' || s[i] == '}') { // 栈空则不匹配 if (v.empty()) { return false; } switch (s[i]) { case ')': { if (v.top() == '(') { v.pop(); } else return false; break; } case ']': { if (v.top() == '[') { v.pop(); } else return false; break; } case '}': { if (v.top() == '{') { v.pop(); } else return false; break; } } } } if (v.empty()) return true; else return false; } };
-
后缀表达式求值
后缀表达式也称逆波兰式。中缀表达式不仅依赖运算符的优先级,而且还要处理括号。后缀表达式的运算符在操作数后面,在后缀表达式中已考虑了运算符的优先级,没有括号,只有操作数和运算符。例如中缀表达式 A + B ∗ ( C − D ) − E / F 所对应的后缀表达式为 A B C D − ∗ + E F / − 。后缀表达式计算规则:从左到右遍历表达式的每个数字和符号,遇到是数字就进栈,遇到是符号,就将处于栈顶两个数字出栈,进项运算,运算结果进栈,一直到最终获得结果。
后缀表达式 A B C D − ∗ + E F / − 求值的过程需要12步,如下表所示:
class Solution { public: int isValid(string s) { stack<int> v; for(int i=0;i<s.size();i++){ //如果是数字字符,则将其压栈(转为int)。isdigit()自己定义的函数 if(isdigit(s[i])) v.push(s[i]-'0'); //是运算符 else{ int val1 = v.top(); v.pop(); int val2 = v.top(); v.pop(); switch(s[i]){ case +: v.push(val1+val2);break; case -: v.push(val1-val2);break; case *: v.push(val1*val2);break; case /: v.push(val1/val2);break; } } } return v.top(); } }
-
中缀表达式转后缀表达式
规则:从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后缀表达式的一部分;若是符号,则判断其与栈顶符号的优先级,是右括号或优先级低于栈顶符号(乘除优先加减)则栈顶元素依次出栈并输出,并将当前符号进栈,一直到最终输出后缀表达式为止。
例:将中缀表达式 a + b − a ∗ ( ( c + d ) / e − f ) + g 转化为相应的后缀表达式。
#include <iostream> #include <stack> // 判断是否是操作符 bool isOperator(char ch) { if(ch == '+' || ch == '-' || ch == '*' || ch == '/') return true; return false; // 否则返回false } // 获取优先级 int getPriority(char ch) { int level = 0; // 优先级 switch(ch) { case '(': level = 1; break; case '+': case '-': level = 2; break; case '*': case '/': level = 3; break; default: break; } return level; } int main(int argc, const char * argv[]) { int num; char arr[250]; // 一个一个的读取表达式,直到遇到'\0' std::stack<char> op; // 栈op:存储操作符 while(1) { std::cin.getline(arr,250); int len, i; char c; // c存储从栈中取出的操作符 len = (int)strlen(arr); // strlen()输出的是:unsigned long类型,所以要强制转换为int类型 i = 0; while(i < len) { if(isdigit(arr[i])) { // 如果是数字 num = 0; //计算第一个数字 do { num = num * 10 + (arr[i] - '0'); // ch - 48根据ASCAII码,字符与数字之间的转换关系 i++; // 下一个字符 }while(isdigit(arr[i])); std::cout << num << " "; } else if(arr[i] == '(') { // (:左括号压栈 op.push(arr[i]); i++; } else if(isOperator(arr[i])) { // 操作符 if(op.empty()) {// 如果栈空,直接压入栈 op.push(arr[i]); i++; } else { // 比较栈op顶的操作符与ch的优先级 // 如果ch的优先级高,则直接压入栈 // 否则,推出栈中的操作符,直到操作符小于ch的优先级,或者遇到(,或者栈已空 while(!op.empty()) { c = op.top(); if(getPriority(arr[i]) <= getPriority(c)) { // 优先级低或等于 std::cout << c << " "; op.pop(); } else // ch优先级高于栈中操作符 break; } // while结束 op.push(arr[i]); // 防止不断的推出操作符,最后空栈了;或者ch优先级高了 i++; } // else } else if(arr[i] == ')') { // 如果是右括号,一直推出栈中操作符,直到遇到左括号( while(op.top() != '(') { std::cout << op.top() << " "; op.pop(); } op.pop(); // 把左括号(推出栈 i++; } else // 如果是空白符,就进行下一个字符的处理 i++; } // 第二个while结束 while(!op.empty()) { // 当栈不空,继续输出操作符 std::cout << op.top() << " "; op.pop(); } std::cout << std::endl; } // 第一个while结束 return 0; }
3.2 队列结构
规则:先进先出,队尾进队头出。
应用
-
循环队列
为了区分队空还是队满的情况,可采取处理牺牲一个单元来区分队空和队满,入队时少用一个队列单元,这是种较为普遍的做法,约定以“队头指针在队尾指针的下一位置作为队满的标志”,如图 ( d2 )所示。队满条件: (Q->rear + 1)%Maxsize == Q->front
队空条件仍: Q->front == Q->rear
队列中元素的个数: (Q->rear - Q ->front + Maxsize)% Maxsize
4. 树
4.1 树的四种遍历
-
层次遍历
// 层序遍历 vector<int> leverOrder(TreeNode* root) { queue<TreeNode*> que; if(root!=nullptr) que.push(root); vector<int> result; while (!que.empty()) { TreeNode* node = que.front(); result.push_back(node->val); if (node->left)que.push(node->left); if (node->right)que.push(node->right); } return result; }
-
前序遍历
//前序递归 void PreOrder(TreeNode* bt) { if (bt) { cout << bt->val; PreOrder(bt->left); PreOrder(bt->right); } }
// 前序非递归 vector<int> PreOrderF(TreeNode* root) { vector<int> result; stack<TreeNode*> st; if (root != nullptr)st.push(root); while (!st.empty()) { TreeNode* node = st.top(); if (node != nullptr) { st.pop(); if (node->right)st.push(node->right); //放右孩子 if (node->left)st.push(node->left); //放左孩子 st.push(node); //放父亲 st.push(nullptr); //放标志位 } else { st.pop();//剔除标志位nullptr node = st.top(); // 取栈顶元素 st.pop(); // 此元素出栈 result.push_back(node->val); } } return result; }
-
中序遍历
//中序递归 void InOrder(TreeNode* bt) { if (bt) { PreOrder(bt->left); cout << bt->val; PreOrder(bt->right); } }
//中序非递归 vector<int> InOrderF(TreeNode* root) { vector<int> result; stack<TreeNode*> st; if (root != nullptr)st.push(root); while (!st.empty()) { TreeNode* node = st.top(); if (node != nullptr) { st.pop(); if(node->right)st.push(node->right); st.push(node); st.push(nullptr); if (node->left)st.push(node->left); } else { st.pop();//剔除标志位nullptr node = st.top(); // 取栈顶元素 st.pop(); // 此元素出栈 result.push_back(node->val); } } return result; }
-
后序遍历
//后序递归 void PostOrder(TreeNode* bt) { if (bt) { PreOrder(bt->left); PreOrder(bt->right); cout << bt->val; } }
//后序非递归 vector<int> PostOrderF(TreeNode* root) { vector<int> result; stack<TreeNode*> st; if (root != nullptr)st.push(root); while (!st.empty()){ TreeNode* node = st.top(); if (node!= nullptr) { st.pop(); st.push(node); st.push(nullptr); if (node->right)st.push(node->right); if (node->left)st.push(node->left); } else { st.pop(); node = st.top(); st.pop(); result.push_back(node->val); } } return result; }
4.2 哈夫曼树与哈夫曼编码
带权路径长度
从根到X结点的带权路径长度就是从根结点到X结点的路径长度与X结点的权值的乘积, 二叉树的带权路径长度就是所有的叶子结点的带权路径长度之和(Weight external Path Length)简称WPL。
哈夫曼树的定义
就是同结点个数的所有的二叉树中,WPL最小的二叉树,HuffmanTree,也叫最优二叉树,所以哈夫曼树不一定是唯一的!
构造哈夫曼树规则
- 在频率集合中找出字符中最小的两个;小的在左边,大的在右边;这两个兄弟组成二叉树。他们的双亲为他们的频率(权值)之和。
- 在频率表中删除此次找到的两个数,并加入此次最小两个数的频率和(把他们的双亲加入,然后排序)。
例:假设(a,b,c,d,e)出现的频率为(18,25,13,12,32),构造哈夫曼树。
- 第一步:选择最小的12,13,再排序(18,25,25,32)
- 第二步,选择18,25构造,再排序(25,32,43)
- 第三步,选择25,32构造,再排序(43,57)
- 第四步:选择43,57构造
对上述进行编码(哈夫曼编码,左子树为边值0,右子树边值为1)
a:00;b:10;c:011;d:010;e:11;
4.3 二叉排序树
- 二叉排序树:BST ,对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。
- 如果有相同的值,可以将该节点放在左子节点或右子节点。
4.4 平衡二叉树
平衡因子:左右子树的高度差。
平衡二叉树:AVL树,各节点的平衡因子的绝对值不超过1的二叉排序树,即平衡因子只能是-1,1,0。
插入平衡二叉树的四种破坏情况:
- LL 型:插入左孩子的左子树,右旋;
- RR 型:插入右孩子的右子树,左旋;
- LR 型:插入左孩子的右子树,先左旋,再右旋;
- RL 型:插入右孩子的左子树,先右旋,再左旋;
4.5 B树
B树:一种平衡的多路查找树,B树中所有结点的子树的最大值称为B树的阶,用m表示。一棵m阶的B树,满足以下特性:
- 树中每个结点最多有m-1个节点,即m棵子树;
- 树中除根结点和叶结点之外,其他结点至少含有ceil(m/2)-1个,即ceil(m/2)棵子树;
- 所有叶结点都在同一层(绝对平衡);
- 所有非叶结点的结构为 [n|p0|k0|…|pn|pk];
4.6 B+树
B树的特点:
- B+树的索引结点并不会保存记录,只用于索引,所有的数据都保存在B+树的叶子结点中;
- B+树的叶子结点都会被连成一条链表。叶子本身按索引值的大小从小到大进行排序;
- m阶的B+树有k个元素(B树中是k-1个元素),且有k个子树;
B+树相比B树的优势:
1.单一节点存储更多的元素,使得查询的IO次数更少;
2.所有查询都要查找到叶子节点,查询性能稳定;
3.所有叶子节点形成有序链表,便于范围查询。
4.7 红黑树:平衡的二叉查找树
特点:
-
结点是红色或黑色。
-
根结点是黑色。
-
每个叶子结点都是黑色的空结点(NIL结点)。
-
每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点)
-
从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点。
例:
简便记法:
根结点必黑,新增为红色;
只能黑连黑,不能红连红;
爸叔通红就变色,爸红叔黑就旋转;
哪边黑往哪边转。
参考链接:
https://blog.csdn.net/bjweimengshu/article/details/106345677?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522164871163716781683912078%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=164871163716781683912078&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-106345677.142^v5^pc_search_result_control_group,143^v6^control&utm_term=%E7%BA%A2%E9%BB%91%E6%A0%91&spm=1018.2226.3001.4187
5. 图
5.1 简介
图的定义:图由顶点的有穷集合V(G)和边的有穷集合E(G)组成,G=(V,E)表示图。
图的表示方法: 邻接矩阵、邻接表
- 邻接矩阵
G=(V,E)是一个n个顶点,e条边的图,则图G的邻接矩阵是一个n*n的二维数组arc。如果(Vi,Vj)是图的一条边,则arc[i][j]=1,否则arc[i][j]=0。例如:
如果是有权图,则按照以下规则。
例:
- 邻接表
顶点用一个一维数组存储(可以用单链表),顶点数组中每个数据元素存储指向第一个邻接点的指针。每个顶点vi的所有邻接点构成一个线性表,用单链表存储。
5.2 遍历
深度优先遍历(DFS)
- 邻接矩阵的DFS
Boolean visited[MAXVEX]; /* 访问标志的数组 */ void DFS(MGraph G, int i) { int j; visited[i] = TRUE; printf("%c ", G.vexs[i]);/* 打印顶点,也可以其它操作 */ for(j = 0; j < G.numVertexes; j++) if(G.arc[i][j] == 1 && !visited[j]) DFS(G, j);/* 对为访问的邻接顶点递归调用 */ } void DFSTraverse(MGraph G) { int i; for(i = 0; i < G.numVertexes; i++) visited[i] = FALSE; /* 初始所有顶点状态都是未访问过状态 */ for(i = 0; i < G.numVertexes; i++) if(!visited[i]) /* 对未访问过的顶点调用DFS,若是连通图,只会执行一次 */ DFS(G, i); }
- 邻接表的DFS
Boolean visited[MAXVEX]; /* 访问标志的数组 */ void DFS(MGraph GL, int i) { EdgeNode *p; visited[i] = TRUE; printf("%c ", GL->adjList[i].data);/* 打印顶点,也可以其它操作 */ p=GL->adjList[i]firstedge; while(p) { if(!visited[p->adjvex]) DFS(GL.P->adjvex); p=p->next; } } void DFSTraverse(MGraphList GL) { int i; for(i = 0; i < GL.numVertexes; i++) visited[i] = FALSE; /* 初始所有顶点状态都是未访问过状态 */ for(i = 0; i < GL.numVertexes; i++) if(!visited[i]) /* 对未访问过的顶点调用DFS,若是连通图,只会执行一次 */ DFS(GL, i);
广度优先遍历(BFS)
邻接表的BFS
void BFSTraverse(MGraph G)
{
int i, j;
Queue Q;
for(i = 0; i < G.numVertexes; i++)
visited[i] = FALSE;
InitQueue(&Q); /* 初始化一辅助用的队列 */
for(i = 0; i < G.numVertexes; i++) /* 对每一个顶点做循环 */
{
if (!visited[i]) /* 若是未访问过就处理 */
{
visited[i]=TRUE; /* 设置当前顶点访问过 */
printf("%c ", G.vexs[i]);/* 打印顶点,也可以其它操作 */
EnQueue(&Q,i); /* 将此顶点入队列 */
while(!QueueEmpty(Q)) /* 若当前队列不为空 */
{
DeQueue(&Q,&i); /* 将队对元素出队列,赋值给i */
for(j=0;j<G.numVertexes;j++)
{
/* 判断其它顶点若与当前顶点存在边且未访问过 */
if(G.arc[i][j] == 1 && !visited[j])
{
visited[j]=TRUE; /* 将找到的此顶点标记为已访问 */
printf("%c ", G.vexs[j]); /* 打印顶点 */
EnQueue(&Q,j); /* 将找到的此顶点入队列 */
}
}
}
}
}
}
5.3 最小生成树
一个有N个点的图的最小生成树,就是在图中所有边中选择出N-1条,连接所有的N个点,着N-1条边的权值之和是所有方法中最小的。
最小生成树算法:Prim算法、Kruskal算法
Prim算法:选点
任选一个顶点V0开始,然后选择V0相连的权值最小的点,重复此过程直到覆盖所有点。
例:
选择V0,在选择于V0连接顶点中权值最小的V6
再选择于V0和V6相邻且权值最小的,即V1
重复以上过程,得到最小生成树如下
Kruskal算法:选边
Kruskal首先将所有的边按从小到大顺序排序(一般使用快排),并认为每一个点都是孤立的,分属于n个独立的集合。然后按顺序枚举每一条边。如果这条边连接着两个不同的集合,那么就把这条边加入最小生成树,这两个不同的集合就合并成了一个集合;如果这条边连接的两个点属于同一集合,就跳过。直到选取了n-1条边为止。
例:
- 开始的时候,认为每个点是孤立的,分属n个独立的集合。有5个集合{{1},{2},{3},{4},{5}}。
- 选择最小的一条边,而且这条边的两个顶点属于两个不同的集合,并将连接的点和为一个集合。集合{{1,2},{3},{4},{5}}
- 再选择剩下的边中最小的且连接两个集合的。集合{{1,2},{3},{4,5}}
- 重复以上步骤,最小生成树
5.4 最短路径
Dijkstra算法
算法过程:
带权图G=(V,E),顶点集合S用来存放已经求得最短路径的所有顶点,集合V-S表示没有确定最短路径的所有顶点集合。逐个将集合V-S中的顶点加入集合S中,直到S中包含图中所有顶点,集合V-S为空集为止。
采用邻接矩阵存储方式,Dijkstra的具体实现过程中维护两个辅助数组。
- Distance[w]数组:表示从顶点V0出发,且只经过集合S中包含的所有顶点,最终达到w的最短路径长度。
- found[i]数组:表示集合S,如果顶点i在S中,found[i]=True,否则found[i]=false。
例:
distance[]和path[]数组变化为表所示。
循环 | S | min | V0 | V1 | V2 | V3 | V4 | V5 |
---|---|---|---|---|---|---|---|---|
初始 | 0 | 5,0 | 30,0 | 35,0 | ∞,0 | ∞,0 | ||
1 | {0} | 1 | 0 | 5,0 | 29,1 | 35,0 | 34,1 | 15,1 |
2 | {0,1} | 5 | 0 | 5,0 | 29,1 | 23,5 | 27,5 | 15,1 |
3 | {0,1,5} | 3 | 0 | 5,0 | 29,1 | 23,5 | 27,5 | 15,1 |
4 | {0,1,5,3} | 4 | 0 | 5,0 | 29,1 | 23,5 | 27,5 | 15,1 |
5 | {0,1,5,3,4} | 2 | 0 | 5,0 | 29,1 | 23,5 | 27,5 | 15,1 |
6 | {0,1,5,3,4,2} | 0 | 5,0 | 29,1 | 23,5 | 27,5 | 15,1 |
由表可知,从V0到各个顶点的最短路径可通过distance和path数组获得。例如distance[2]=29,得知V0→V2的最短路径长度为29,由path[2]=1和path[1]=0,得出V0到V2的最短路径为(V0,V1,V2) 。再例如,distance[4]=27,得知V0→V2的最短路径长度为27,path[4]=5,path[5]=1,path[1]=0,得知V0到V4的最短路径为(V0,V1,V5,V4)。
Floyd算法
Dijkstra算法主要用于求单源最短路径,Floyd算法用于求多源最短路径。
Floyd算法的主要思想是动态规划(dp)。算法的具体思想为:
- 邻接矩阵(二维数组)dist储存路径,数组中的值开始表示点点之间初始直接路径,最终是点点之间的最小路径,有两点需要注意的,第一是如果没有直接相连的两点那么默认为一个很大的值(不要因为计算溢出成负数),第二是自己和自己的距离要为0。
- 从第1个到第n个点依次加入松弛计算,每个点加入进行试探枚举是否有路径长度被更改(自己能否更新路径)。顺序加入(k枚举)松弛的点时候,需要遍历图中每一个点对(i,j双重循环),判断每一个点对距离是否因为加入的点而发生最小距离变化,如果发生改变(变小),那么两点(i,j)距离就更改。
- 重复上述直到最后插点试探完成。
其中第2步的状态转移方程为:
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j])//dp[i][j]表示从i到j的最短路径
例:
- 初始状态
- 加入A更新。
- 加入B更新。
- 加入C更新。
- 加入D更新。
- 加入E更新
- 加入F更新。
- 加入G更新。
5.5 拓扑排序(AOV网)
拓扑排序是一个有向无环图的所有顶点的线性序列,且该序列需满足以下两个条件:
- 每个顶点出现且只出现一次;
- 若存在一条从顶点A到B的路径,那么在序列中顶点A出现在顶点B的前面。
注:只有有向无环图才有拓扑排序。
拓扑排序过程:
- 从 DAG 图中选择一个 没有前驱(即入度为0)的顶点并输出。
- 从图中删除该顶点和所有以它为起点的有向边。
- 重复 1 和 2 直到当前的 DAG 图为空或当前图中不存在无前驱的顶点为止。后一种情况说明有向图中必然存在环。
例:
拓扑排序过程:
拓扑排序的结果: { 1, 2, 4, 3, 5 }
5.6 关键路径(AOE网)
关键路径简介
路径上各个活动所持续的时间之和称为路径长度,从源点到汇点具有最大长度的路径叫关键路径。关键路径上的活动称为关键活动。
如图所示,边A1→A6表示学习的课程,也就是“活动“,边权表示课程学习需要的时间;顶点V1→V6表示到目前位置的课程已经学完,也就是”事件“。
在AOE网中仅有一个入度为0的顶点,称为开始顶点(源点),它表示整个工程的开始;网中也仅存在一个出度为0的顶点,称为结束顶点(汇点),它表示整个工程的结束。
关键路径算法
五个变量定义:
- 事件的最早发生时间(Ve(i)):事件Vi的最早发生时间是从顶点V0到顶点Vi的最长路径长度。计算时采用正向递推方式。max
- 事件的最迟发生时间(Vl(i)):事件Vi的最迟发生时间指在不推迟整个工期的前提下事件Vi可发生的最晚事件。计算时采用反向地推方式。min
- 活动的最早发生时间(e(i)):表示指该活动的起点所表示事件最早发生时间。如果< Vi,Vj >表示活动ai,则e(i)=Ve(i)。
- 活动的最迟发生时间(l(i)):表示该活动的终点所表示的事件最迟发生时间与该活动所需时间之差。如果边< Vi,Vj >表示活动ai,则有l(i)=vl(j)-weight(Vi,Vj)。
- 活动的时间余量:d[i] = l(i)-e(i)。
例:
事件时间 | V0 | V1 | V2 | V3 |
---|---|---|---|---|
Ve | 0 | 4 | 6 | 10 |
Vl | 0 | 4 | 6 | 10 |
活动时间 | a0 | a1 | a2 | a3 | a4 |
---|---|---|---|---|---|
e | 0 | 0 | 4 | 6 | 4 |
l | 0 | 3 | 4 | 6 | 4 |
d | 0 | 3 | 0 | 0 | 0 |
d为0的为关键路径,即a0,a2,a3,a4。由图可知,此图有两条关键路径,分别为,a0→a2→a3和 a0→a4。
6. 散列表
6.1简介
散列表(哈希表),将一组关键字(可以是数字、字符串或其他),通过散列函数h(key)转换为key在表中的存储位置。即 存储位置=h(key)。
例:关键字集合{12,7,26,40,16,34,18},散列函数h(key)=key mod 13,构造散列表。
-
计算关键字的散列地址{12,7,0,1,3,8,5}。
-
构建表
散列地址 0 1 2 3 4 5 6 7 8 9 10 11 12 关键字 26 40 16 18 7 34 12
散列表查找过程:
- 根据散列函数 f 找到给定值key的映射f(key)
- 若找到集合中存在这个记录,则必定在f(key)的位置上。
6.2 散列函数
设计原则:
- 确定:同一关键字总是映射到固定地址,即散列地址完全由关键字确定。
- 快速:散列函数的计算要简单,减少查找的时间开销。
- 均匀:散列后的地址尽量散列均匀分布在整个散列地址空间,减少冲突
装载因子
装载因子=填入数组的元素个数/散列数组长度;
6.3 冲突
解决冲突的方法有两大类:开地址法、拉链法。
开地址法
(1)线性探测:冲突位置向后找
(2)二次探测:平方位置前后找,(1,-1,4,-4,…)
(3)双重散列:多个散列函数
拉链法
散列表中,每个“桶(bucket)”会对应一条链表,所有散列值相同的元素我们都放到相同槽位对应的链表中:
7. 排序
排序方法 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
插入排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 |
希尔排序 | O(n) | O(n) | O(n²) | O(1) | 不稳定 |
选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
冒泡排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 |
快速排序 | O(nlogn) | O(nlogn) | O(n²) | O(nlogn) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
基数排序 | O(d*(r+n)) | O(d*(r+n)) | O(d*(r+n)) | O(d*r+n) | 稳定 |
桶排序 | O(M+N) | O(M+N) | 取决于桶内的排序方法 | O(M+N) | 稳定 |
参考链接:https://blog.csdn.net/yunqiinsight/article/details/108662359?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522164959614616782094840960%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=164959614616782094840960&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-108662359.142^v7^control,157^v4^control&utm_term=%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95%E6%80%BB%E7%BB%93&spm=1018.2226.3001.4187
插入排序
void insertSort(int* arr, int len) {
if (len < 2)return;
int t;
for (int i = 1; i < len; i++) {
t = arr[i];//待排序元素
// 从已排序的部分从后向前找位置
for (int j = i - 1; j >= 0; j--) {
if (arr[j] <= t)break;//小于或等于,即找到了插入位置
arr[j + 1] = arr[j];//元素后移
}
arr[i + 1] = t;
}
}
选择排序
void selectSort(int* arr, int len) {
if (len < 2)return;
int min;
for (int i = 0; i < len - 1; i++) {
min = i;
for (int j = i + 1; j < len; j++) {
if (arr[j] < arr[min])min = j;
}
if (min != i)swap(arr[i],arr[min]);
}
}
冒泡排序
void bubbleSort(int *arr,int len) {
if (len < 2)return;
int t;
for (int j = len - 1; j > 0; j--) {
for (int i = 0; i < j; i++) {
if (arr[j] > arr[i]) {
t = arr[j];
arr[j] = arr[i];
arr[i] = t;
}
}
}
}
快速排序
// 一次快排
int OneQuickSort(int A[], int left, int right) {
int temp = A[left];
while (left<right)
{
while (left<right && A[right] >= temp)right--;
A[left] = A[right];
while (left<right && A[left] <= temp)left++;
A[right] = A[left];
}
A[left] = temp;
return left;
}
void QuickSort(int A[],int left,int right) {
if (left < right) {
int pos = OneQuickSort(A,left,right);
QuickSort(A, left, pos - 1);
QuickSort(A, pos + 1, right);
}
}
8. 其他
8.1 朴素的模式匹配算法
原串:t=“ABABBABABACDDAB”。模式p=“ABABACDD”。
int bruteForce(string s, string t, int pos) {
int i = pos, j = 0;
while (i < s.size() && j < t.size()) {
if (s[i] == t[j]) {
i++;
j++;
} else {
i = i - j + 1;
j = 0;
}
}
return j >= t.size() ? i - t.size() : 0;
}