山东大学软件学院2023-2024数据结构期末复习

数据结构期末算法复习笔记

文章目录

一、线性结构

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)=(i1u2u3...uk)+(i2u3...uk)+...+(ik1uk)+(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)=(jkuk1...u1)+(jk1uk2...u1)+...+(j2u1)+(j1)

3.比较数组和链表的优缺点

  • 数组

    优点:

    • 随机访问性强,查找速度快,时间复杂度为O(1)

    缺点:

    • 插入和删除效率低,时间复杂度为O(N)
    • 空间利用率不高
    • 内存空间要求高,必须有足够的连续的内存空间
    • 数组空间的大小固定,不能动态拓展
  • 链表

    优点:

    • 任意位置插入元素和删除元素的速度快,时间复杂度为O(1)
    • 内存利用率高,不会浪费内存
    • 链表的空间大小不固定,可以动态拓展

    缺点:

    • 随机访问效率低,时间复杂度为O(N)

4.跳表和散列的区别和联系

  • 联系:跳表和哈希表都是为了解决数据的快速查找问题。跳表主要用于解决链表数据结构的查找慢的问题,而哈希表是利用了数组按照下标随机访问的特性,通过哈希函数将要查找的键映射到一个下标,然后去这个下标处取数据
  • 区别:
    • 跳表:
      • 跳表是一种链表加多索引的结构,它支持快速的插入、删除、查找操作
      • 在跳表中查询某个数据的时候,如果每一层都要遍历m个结点,那在跳表中查询一个数据的时间复杂度就是O(m*logn)
      • 跳表占用内存但是速度快
    • 散列:
      • 哈希表,也被称为散列表,它是数组的一种扩展,由数组演化而来
      • 哈希表用的是数组支持按照下标随机访问数据的特性
      • 哈希表的主要优点是查找、插入、删除的时间复杂度接近O(1),但是哈希表的大小是固定的,如果哈希表已满,需要进行扩容,这个过程的时间复杂度是O(n)

二、层次结构

1.二叉树遍历:

  • 前序遍历:先自己,再左,再右
  • 中序遍历:先左,再自己,再右
  • 后序遍历:先左,再右,再自己

2.构建哈夫曼树:

从集合中选择两棵具有最小权值的二叉树,并把它们合并成一棵新的二叉树。合并方法是把这两棵二叉树分别作为左右子树,然后增加一个新的根节点。新二叉树的权值为两棵子树的权值之和。编码:左子树为0,右子树为1.

hfm1hfm2

3.大根堆

(1)初始化:将所有元素依次放入完全二叉树,从最后一个有子节点的节点开始调整二叉树
(2)插入:将要插入元素添加至末尾,与父节点比较,若大于父节点,交换

dgdcr

(3)堆排序:先初始化为大根堆,然后让根节点pop,子节点中大的节点向上补位,最后空缺补最小的节点

dgdpx

(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;
    }
}

dw0

在二叉搜索树中删除节点(度为 1 )

bst_remove_case3_step2

bst_remove_case3_step4

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);
            }
        }
    }
}

原创内容,请勿转载!

  • 22
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值