数据结构期末算法复习笔记
文章目录
一、线性结构
1.排序算法的基本思想(以从小到大为例)
(1)冒泡排序
如果相邻两个元素顺序不对,则交换这两个元素,如果顺序正确,则不交换。执行n次。
(2)选择排序
从未排序的序列中,找出最小的元素,与未排序的第一个元素交换。
(3)插入排序
类似扑克牌的整理,从第二个元素开始,与已排序的序列元素从最大那个开始比较,若小于此元素,则此元素向后移动一位,若不小于此元素,则放在此元素后面
void insertionSort(int* a, int len){
for (int i = 1; i < len; i++)
{
int key = a[i];
int j = i - 1;
while (j >= 0 && key < a[j])
{
a[j + 1] = a[j];
j--;
}
a[j + 1] = key;
}
}
(4)归并排序
利用分而治之算法,从中点开始,将数组划分为两个子集,先对左边子集进行递归排序,再对右边子集进行递归排序,最后合并
void mergeSort(vector<int> &nums, int left, int right) {
// 终止条件
if (left >= right)
return; // 当子数组长度为 1 时终止递归
// 划分阶段
int mid = (left + right) / 2; // 计算中点
mergeSort(nums, left, mid); // 递归左子数组
mergeSort(nums, mid + 1, right); // 递归右子数组
merge(nums, left, mid, right); // 合并
}
(5)快速排序
以第一个为基准数,将之放到正确的位置,使基准数左边元素都小于它,右边元素都大于它。递归左子数组、右子数组
- 找到基准数正确位置的方法:定义两个指针left和right,先让left向右走,直到找到大于支点元素为止,再让right向左走,直到找到小于支点元素为止,将两个元素交换。循环执行此过程,直到left≥right为止,让支点元素与right元素交换
void quickSort(vector<int> &nums, int left, int right) {
// 子数组长度为 1 时终止递归
if (left >= right)
return;
// 哨兵划分int partition(vector<int> &nums, int left, int right),返回基准数的索引
int pivot = partition(nums, left, right);
// 递归左子数组、右子数组
quickSort(nums, left, pivot - 1);
quickSort(nums, pivot + 1, right);
}
(6)拓扑排序
依次写出每个入度为0的点,并将这个点删去
(7)基数排序
先排个位,再排十位,再排百位…
(8)堆排序
利用大根/小根堆进行排序
2.k维数组行主映射、列主映射公式
-
行主映射:
m a p ( i 1 , i 2 , . . . , i k ) = ( i 1 ∗ u 2 ∗ u 3 ∗ . . . ∗ u k ) + ( i 2 ∗ u 3 ∗ . . . ∗ u k ) + . . . + ( i k − 1 ∗ u k ) + ( i k ) map(i_1,i_2,...,i_k)=(i_1*u_2*u_3*...*u_k)+(i_2*u_3*...*uk)+...+(i_{k-1}*u_k)+(i_k) map(i1,i2,...,ik)=(i1∗u2∗u3∗...∗uk)+(i2∗u3∗...∗uk)+...+(ik−1∗uk)+(ik) -
列主映射:
m a p ( j 1 , j 2 , . . . , j k ) = ( j k ∗ u k − 1 ∗ . . . ∗ u 1 ) + ( j k − 1 ∗ u k − 2 ∗ . . . ∗ u 1 ) + . . . + ( j 2 ∗ u 1 ) + ( j 1 ) map(j_1,j_2,...,j_k)=(j_k*u_{k-1}*...*u_1)+(j_{k-1}*u_{k-2}*...*u1)+...+(j_2*u_1)+(j_1) map(j1,j2,...,jk)=(jk∗uk−1∗...∗u1)+(jk−1∗uk−2∗...∗u1)+...+(j2∗u1)+(j1)
3.比较数组和链表的优缺点
-
数组
优点:
- 随机访问性强,查找速度快,时间复杂度为O(1)
缺点:
- 插入和删除效率低,时间复杂度为O(N)
- 空间利用率不高
- 内存空间要求高,必须有足够的连续的内存空间
- 数组空间的大小固定,不能动态拓展
-
链表
优点:
- 任意位置插入元素和删除元素的速度快,时间复杂度为O(1)
- 内存利用率高,不会浪费内存
- 链表的空间大小不固定,可以动态拓展
缺点:
- 随机访问效率低,时间复杂度为O(N)
4.跳表和散列的区别和联系
- 联系:跳表和哈希表都是为了解决数据的快速查找问题。跳表主要用于解决链表数据结构的查找慢的问题,而哈希表是利用了数组按照下标随机访问的特性,通过哈希函数将要查找的键映射到一个下标,然后去这个下标处取数据
- 区别:
- 跳表:
- 跳表是一种链表加多索引的结构,它支持快速的插入、删除、查找操作
- 在跳表中查询某个数据的时候,如果每一层都要遍历m个结点,那在跳表中查询一个数据的时间复杂度就是O(m*logn)
- 跳表占用内存但是速度快
- 散列:
- 哈希表,也被称为散列表,它是数组的一种扩展,由数组演化而来
- 哈希表用的是数组支持按照下标随机访问数据的特性
- 哈希表的主要优点是查找、插入、删除的时间复杂度接近O(1),但是哈希表的大小是固定的,如果哈希表已满,需要进行扩容,这个过程的时间复杂度是O(n)
- 跳表:
二、层次结构
1.二叉树遍历:
- 前序遍历:先自己,再左,再右
- 中序遍历:先左,再自己,再右
- 后序遍历:先左,再右,再自己
2.构建哈夫曼树:
从集合中选择两棵具有最小权值的二叉树,并把它们合并成一棵新的二叉树。合并方法是把这两棵二叉树分别作为左右子树,然后增加一个新的根节点。新二叉树的权值为两棵子树的权值之和。编码:左子树为0,右子树为1.
3.大根堆
(1)初始化:将所有元素依次放入完全二叉树,从最后一个有子节点的节点开始调整二叉树
(2)插入:将要插入元素添加至末尾,与父节点比较,若大于父节点,交换
(3)堆排序:先初始化为大根堆,然后让根节点pop,子节点中大的节点向上补位,最后空缺补最小的节点
(4)删除最大元素pop:将根节点的大孩子作为新根节点
4.二叉树高度
int getHeight(Node* root) {
if (root == nullptr) {
return 0;
} else {
int leftHeight = getHeight(root->left);
int rightHeight = getHeight(root->right);
return std::max(leftHeight, rightHeight) + 1;
}
}
5.二叉树度为1的节点个数和度为2的节点个数
int countDegreeOneNodes(Node* root) {
if (root == nullptr) {
return 0;
}
int count = 0;
if ((root->left == nullptr && root->right != nullptr) || (root->left != nullptr && root->right == nullptr)) {
count = 1;
}
count += countDegreeOneNodes(root->left) + countDegreeOneNodes(root->right);
return count;
}
int countDegreeTwoNodes(Node* root) {
if (root == nullptr) {
return 0;
}
int count = 0;
if (root->left != nullptr && root->right != nullptr) {
count = 1;
}
count += countDegreeTwoNodes(root->left) + countDegreeTwoNodes(root->right);
return count;
}
6.统计二叉树中节点个数
int countLeafNodes(Node* root) {
if (root == nullptr) {
return 0;
}
if (root->left == nullptr && root->right == nullptr) {
return 1;
}
return countLeafNodes(root->left) + countLeafNodes(root->right);
}
7.二叉搜索树:左孩子<自己<右孩子
(1)搜索某个元素:利用二叉搜索树的性质
TreeNode *search(int num) {
TreeNode *cur = root;
// 循环查找,越过叶节点后跳出
while (cur != nullptr) {
// 目标节点在 cur 的右子树中
if (cur->val < num)
cur = cur->right;
// 目标节点在 cur 的左子树中
else if (cur->val > num)
cur = cur->left;
// 找到目标节点,跳出循环
else
break;
}
// 返回目标节点
return cur;
}
(2)插入元素(不允许重复):用搜索的方法找到应该放的位置
void insert(int num) {
// 若树为空,则初始化根节点
if (root == nullptr) {
root = new TreeNode(num);
return;
}
TreeNode *cur = root, *pre = nullptr;
// 循环查找,越过叶节点后跳出
while (cur != nullptr) {
// 找到重复节点,直接返回
if (cur->val == num)
return;
pre = cur;
// 插入位置在 cur 的右子树中
if (cur->val < num)
cur = cur->right;
// 插入位置在 cur 的左子树中
else
cur = cur->left;
}
// 插入节点
TreeNode *node = new TreeNode(num);
if (pre->val < num)
pre->right = node;
else
pre->left = node;
}
(3)删除元素:度为0时直接删除,度为1时让子节点替换之,度为2时找到中序遍历的后继节点,让它替换之
/* 删除节点 */
void remove(int num) {
// 若树为空,直接提前返回
if (root == nullptr)
return;
TreeNode *cur = root, *pre = nullptr;
// 循环查找,越过叶节点后跳出
while (cur != nullptr) {
// 找到待删除节点,跳出循环
if (cur->val == num)
break;
pre = cur;
// 待删除节点在 cur 的右子树中
if (cur->val < num)
cur = cur->right;
// 待删除节点在 cur 的左子树中
else
cur = cur->left;
}
// 若无待删除节点,则直接返回
if (cur == nullptr)
return;
// 子节点数量 = 0 or 1
if (cur->left == nullptr || cur->right == nullptr) {
// 当子节点数量 = 0 / 1 时, child = nullptr / 该子节点
TreeNode *child = cur->left != nullptr ? cur->left : cur->right;
// 删除节点 cur
if (cur != root) {
if (pre->left == cur)
pre->left = child;
else
pre->right = child;
} else {
// 若删除节点为根节点,则重新指定根节点
root = child;
}
// 释放内存
delete cur;
}
// 子节点数量 = 2
else {
// 获取中序遍历中 cur 的下一个节点
TreeNode *tmp = cur->right;
while (tmp->left != nullptr) {
tmp = tmp->left;
}
int tmpVal = tmp->val;
// 递归删除节点 tmp
remove(tmp->val);
// 用 tmp 覆盖 cur
cur->val = tmpVal;
}
}
8.AVL树:平衡搜索树,|height(left)-height(right)|≤1,添加/删除节点会导致树失衡
平衡因子:左子树的高度-右子树的高度
(1)左孩子的左子树太高了,进行一次右旋
(2)右孩子的右子树太高了,进行一次左旋
(3)左孩子的右子树太高了,进行一次左旋和一次右旋
(4)右孩子的左子树太高了,进行一次右旋和一次左旋
三、网状结构
1.判断是否为连通图:对图进行广度优先遍历/深度优先遍历,有没遍历过的点就是非连通图
bool connected()
{ //当且仅当图是连通的,则返回true
if (directed()) throw ……//如果图不是无向图, 抛出异常;
int n =numberofVertices();//图中顶点数
//置所有顶点为未到达顶点
int *reach = new int [n+1];
for (int i = 1; i <= n; i++)
reach[i] = 0;
//对从顶点1出发可到达的顶点进行标记
dfs(1, reach, 1);
//检查是否所有顶点都已经被标记
for (int i = 1; i <= n; i++)
if (reach[i]==0) return false;
return true;
}
2.连通构件数目:遍历c数组,有没有遍历的点label就++,再以那个点为起点遍历,将标记设为label
int labelcomponents(int c[])
{ // 构件标识, 返回构件的数目,并用c[1:n]表示构件编号
if (directed()) throw ……//如果图不是无向图, 抛出异常;
int n = numberofVertices();//图中顶点数
// 初始时,所有顶点都不属于任何构件
for (int i = 1; i <= n; i++)
c[i] = 0;
int label = 0; // 最后一个构件的编号
// 识别构件
for (int i = 1; i <= n; i++)
if (c[i]==0) //顶点i未到达
// 顶点i 属于一个新的构件
{label++ ;
bfs(i, c, label);} // 标记新构件
return label;
}
3.判断是否有环:使用并查集,右边则把两个集合合并成一个,若两条边连接同一集合的元素,则有环
int find(int node) {
if (parent[node] == node) {
return node;
}
return parent[node] = find(parent[node]);
}
bool unionNodes(int node1, int node2) {
int root1 = find(node1);
int root2 = find(node2);
if (root1 == root2) {
return false;
}
parent[root1] = root2;
return true;
}
bool isCyclic(vector<pair<int, int>>& edges) {
int numNodes = edges.size();
parent.resize(numNodes);//Vector类,将容器大小设置为numNodes
for (int node = 0; node < numNodes; ++node) {
parent[node] = node;
}//将每个点的根元素赋值为自己
for (auto& edge : edges) {
if (!unionNodes(edge.first, edge.second)) {
return true;
}
}//对每个点执行合并,若合并失败,则有环
return false;
}
4.Dijkstra算法的大致思想(求单源最短路径)
定义数组visited,dis,front
visited:表示当前节点是否被遍历到
dis:节点到源点距离,初始值:邻居为权值,非邻居为INF,源点为0
front:前置节点,默认为-1
找到visited为false的节点中dis最小的顶点A,考虑A的所有邻居,若邻居的dis值大于现在访问的点的dist值+与这个邻居的距离,则让这个邻居的dis=现在访问的点的dist值+与这个邻居的距离;front值为当前访问顶点
重复执行,直到所有顶点的visited均为true
5.Kruskal算法的大致思想(从边的角度求最小生成树问题)
令T为所选边的集合,初始化T =∅
令E为网络中边的集合
While (E≠∅ && |T| ≠ n-1){
令(u,v)为E中代价最小的边
E=E-{(u,v) } //从E中删除边
If ( (u,v)加入T中不会产生环路) 将(u,v)加入T}
if (|T| = = n-1) T是最小耗费生成树
Else 网络不是互连的,不能找到生成树
6.Prim算法大致思想(从点的角度求最小生成树问题)
//假设网络中至少具有一个顶点
设T 为所选择的边的集合,初始化T=∅
设TV 为已在树中的顶点的集合,置TV= { 1 }
令E 为网络中边的集合
While (E≠∅ ) & & (|T| ≠ n-1) {
令(u , v)为最小代价边,其中u ∈TV, v ∉TV
If (没有这种边) break
E=E - { (u,v) } / /从E中删除此边
在T 中加入边( u , v)
}
if (|T| = =n- 1 ) T是一棵最小生成树
else 网络是不连通的,没有最小生成树
7.Flod算法的大致思想(多源最短路径):假设A是中间节点,求BA+AC和BC的大小关系
8.图的广度优先遍历伪代码
BFS(int s){
visited数组全为0;
创建队列Q;
visited[s]=1;
将s入队;
while(Q非空){
v=Q.front();
Q.pop();
while(u赋值为v的邻接节点!=空){
if(visited[u]==0){
visited[u]=1;
Q.push(u);
}
}
}
}
9.图的深度优先遍历
DFS(int v){
visited数组全为0;
stack S;
S.push(v);
while(S不为空){
u=S.top();
visited[u]=1;
S.pop();
while(w赋值为u的邻接节点){
if(visited[w]==0){
S.push(w);
}
}
}
}
原创内容,请勿转载!