[备战软考]数据结构与算法基础

本文全面介绍了数据结构与算法的基础知识,涵盖了线性表、链表、栈、队列、树、二叉树、图等多种数据结构及其操作,以及排序、查找等经典算法,并深入探讨了哈希表的构造及冲突处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

数据结构与算法基础

线性表

1.顺序表

顺序的存储结构,元素在内存中以顺序存储。内存中占用连续的一个区域。

  • 顺序表的删除
    把要删除的元素后面每个元素向前移动一位

  • 顺序表的插入
    把要插入的位置后面的(包括自己)所有元素向后移动一位,再把要插入的元素放入该位置。

2.链表

离散的存储结构,各个点的存储空间是离散的,通过指针联系起来,从而成为一个整体的链表。

  • 单链表
    从第一个元素开始指向下一个元素,最后一个元素指向NULL

  • 循环链表
    最后一个元素指向头

  • 双链表
    两个指针域,从两个方向连接

链表的操作
  • 单链表的结点删除
    前驱指向后继
    Node* a1=(Node*)malloc(sizeof(Node));
    Node* a2=(Node*)malloc(sizeof(Node));
    Node* a3=(Node*)malloc(sizeof(Node));
    a1->next=a2;
    a2->next=a3;
    a3->next=NULL;
    cout<<a1->next<<endl;//未删除时

    //删除a2这个结点
    //关键步骤:将前一个元素的结点的next指向下一个节点
    a1->next=a2->next;
    //将删除的结点的内存释放
    free(a2);
    cout<<a1->next<<endl;
  • 单链表的结点插入
    ①将新的结点指向后一个元素
    ②前一个结点指向要插入的结点
    (顺序不能颠倒,否则下一个结点的地址会找不到!)
    Node* a1=(Node*)malloc(sizeof(Node));
    Node* a2=(Node*)malloc(sizeof(Node));
    a1->next=a2;
    a2->next=NULL;
    //cout<<a1->next<<' '<<a2<<endl;
    //插入x这个结点至a1与a2中间
    Node* x=(Node*)malloc(sizeof(Node));
    x->next=a1->next;//第一步
    a1->next=x;
    //cout<<a1->next<<' '<<x<<endl;
  • 双链表的结点插入和删除
    也是参照单链表的方法,做两次操作而已。但是都要先进行完第一步,再进行第二步。

3.顺序表与链表的比较

  • 空间性能
项目顺序存储链式存储
存储密度=1,更优<1
容量分配事先确定动态改变,更优


  • 时间性能
项目顺序存储链式存储
查找运算O(n/2)O(n/2)
读运算O(1),更优O([n+1]/2),最好1,最坏n
插入运算O(n/2),最好0,最坏nO(1),更优
删除运算O([n-1]/2)O(1),更优


4.栈

先进后出,只能对栈顶进行操作。可以用顺序表和链表实现。

5.队列

先进先出,只能从队尾插入,对头读取。

循环队列

头指针:head 尾指针:tail
如果没有任何元素,head=tail,如果有元素入队,tail向后移一位
如果在最后一个位置也插入了元素,那么tail又会回到head的位置。
为了避免队列空和队列满是一个状态,将最后一个元素的位置舍弃不用。



树和二叉树

1.基本概念

  • 结点的度:与下一层有几个结点相关联,它的度就是多少

  • 树的度:整个树中度数最大的结点的度是多少,树的度就是多少

  • 叶子结点:度为0的结点

  • 分支结点:除了叶子结点的所有结点都是分支结点(下一层有分支)

  • 内部结点:分支结点中除了根结点的所有结点都是内部结点(中间层的结点)

  • 父结点

  • 子节点

  • 兄弟结点

  • 层次

公式:所有结点的度之和+1=结点总个数

2.树的遍历

这里写图片描述

  • 前序遍历:先访问根节点,再依次访问子结点(访问完了一个子结点在访问后一个子结点)

    1 2 5 6 7 3 4 8 9 10

  • 后序遍历:先访问子结点,再访问根结点

    5 6 7 2 3 9 10 8 4 1

  • 层次遍历:一层一层地访问

    1 2 3 4 5 6 7 8 9 10

3.二叉树

每个结点最多只能有两个子结点,分为左子结点和右子结点。

  • 满二叉树:二叉树的每层都是满的(完整金字塔形状)

  • 完全二叉树:对于n层的二叉树,其n-1层是满二叉树,第n层的结点从左到右连续排列

4.二叉树的遍历

与树的遍历是一样的,就是多了一种中序遍历

  • 中序遍历:先访问左子结点,再访问根节点,再访问右子结点

5.查找二叉树(二叉排序树)

空树或满足以下递归条件:

  • 查找树的左右子树各是一颗查找树

  • 若左子树非空,则左子树上的各个结点的值均小于根节点的值

  • 若右子树非空,则左子树上的各个结点的值均大于于根节点的值
基本操作
  • 查找:比较当前结点的值与键值,若键值小,则进入左子结点,若键值大,则进入右子结点

  • 插入结点:

    • 如果相同键值的结点已经在查找二叉树中,则不再插入
    • 如果查找二叉树为空树,则以新结点为查找二叉树
    • 比较插入结点的键值与插入后的父节点的键值,就能确定新结点是父节点的左子结点还是右子结点,并插入
  • 删除操作
    • 若删除的结点p是叶子结点,则直接删除
    • 若p只有一个子结点,则将这个子结点与待删除的结点的父节点直接连接,然后删除节点p
    • 若p有两个子结点,在左子树上,用中序遍历找到关键值最大的结点s,用s的值代替p的值,然后删除结点s,结点s必须满足上面两种情况之一

6.最优二叉树(哈夫曼树)

基本概念
  • 树的路径长度:到达每个叶子结点所需要的长度之和
  • 权:人为定义的每个结点的值
  • 带权路径长度:路径长度*该结点的权值
  • 树的带权路径长度(树的代价):每个叶子结点的带权路径长度之和
构造哈夫曼树

①把每个权值作为根节点,构造成树
②选择两颗根节点最小的树作为子树合成一颗新的树,根节点的值为两个根节点值的和
③重复②,直到只剩一棵树为止

7.线索二叉树

  • 表示
    [Lbit][Lchild][Data][Rchild][Rbit]

标志域规定:
Lbit=0,Lchild是通常的指针
Lbit=1,Lchild是线索(指向前驱)
Rbit=0,Rchild是通常的指针
Rbit=1,Rchild是线索(指向后继)

将二叉树转化为线索二叉树

这里写图片描述
对于每个空余的左右指针,都用线索替代,左指针指向前驱,右指针指向后继

前序:A B D E H C F G I

这里写图片描述
中序:D B H E A F C G I
这里写图片描述

后序:D H E B F I G C A

这里写图片描述

8.平衡二叉树

对某个数列构造排序二叉树,可以构造出多颗形式不同的排序二叉树

  • 定义:树中任一结点的左、右子树的深度相差不超过1
平衡树调整

浅谈算法和数据结构: 九 平衡查找树之红黑树

  • LL型平衡旋转(单向右旋平衡处理)
  • RR型平衡旋转(单向左旋平衡处理)
  • LR型平衡旋转(双向旋转,先左后右)
  • RL型平衡旋转(双向旋转,先右后左)

1.基本概念

  • 图的构成:
    图由两个集合:V和E所构成,V是非空点集,E是边集,图 G=(V,E)

  • 无向图和有向图:
    边是单向的是有向图,双向的就是无向图

  • 顶点的度
    无向图:有几条边相连度就为几
    有向图:分为入度和出度

  • 子图

  • 完全图
    无向图中每对顶点都有一条边相连,有向图中每对顶点都有两条有向边相互连接

  • 路径和回路

  • 连通图
    有向图中,任意两点都有路径到达
    无向图中,没有孤立点的图

  • 强连通
    有向图中,任意两点作为起点和终点都有路径到达则为强连通。如果只能确保单向连通,则是弱连通。

  • 连通分量
    图的一个子图是连通图,那么这个子图就是一个连通分量

  • 网络
    每一条边都有一个权值


2.图的存储

(此部分图片来自刘伟老师)

这里写图片描述

邻接矩阵

这里写图片描述

邻接表

又叫邻接链表
这里写图片描述


3.图的遍历

深度优先(DFS)
  • 首先访问一个未访问的节点V
  • 依次从V出发搜索V的每个邻接点W
  • 若W未访问过,则从该点出发继续深度优先遍历
#include <iostream>
#include <cstring>
#define mem(a,b) memset(a,b,sizeof(a))

using namespace std;

const int maxn=100;

struct EDG
{
    int u;
    int v;
    int w;
    //初始化列表
    EDG(int uu=0,int vv=0,int ww=0):u(uu),v(vv),w(ww){}
}e[maxn];
int first[maxn],nxt[maxn*2];
int vis[maxn];
int len=0;
void mk_edg(int u,int v,int w)//加入u到v权值为w的边
{
    e[++len]=EDG(u,v,w);
    nxt[len]=first[u];
    first[u]=len;
    e[++len]=EDG(v,u,w);
    nxt[len]=first[v];
    first[v]=len;
}

//图的深度优先遍历(递归写法)
void DFS(int v)
{
    vis[v]=1;
    cout<<v<<' ';
    for(int i=first[v];i!=-1;i=nxt[i])
    {
        if(!vis[e[i].v])
        {
            DFS(e[i].v);
        }
    }
}

int main()
{
    mem(first,-1),mem(nxt,-1),mem(vis,0);
    int n,m;
    cin>>n>>m;
    for(int i=0;i<m;i++)
    {
        int u,v,w;
        cin>>u>>v>>w;
        mk_edg(u,v,w);
        mk_edg(v,u,w);
    }
    for(int i=0;i<n;i++)
    {
        if(!vis[i])
            DFS(i);
    }
    return 0;
}
广度优先(BFS)
  • 首先访问一个未访问的顶点V
  • 然后访问与顶点V邻接的全部未访问顶点W、X、Y……
  • 然后再依次访问W、X、Y邻接的未访问顶点


4.最小生成树

(此部分代码来自刘伟老师)

Prim算法

思想:
(1) 任意选定一点s,设集合S={s}
(2) 从不在集合S的点中选出一个点j使得其与S内的某点i的距离最短,则(i,j)就是生成树上的一条边,同时将j点加入S
(3) 转到(2)继续进行,直至所有点都己加入S集合

#include<iostream>  
using namespace std; 

#define MAXN 2001
#define INF 1000000

int n, m;
int G[MAXN][MAXN];  //存储图

void init(){
    for(int i = 0 ; i < n ; i++){  
       for(int j = 0 ; j < n ; j++)  
           G[i][j] = INF;    //初始化图中两点间距离为无穷大 
    }
}

void prim(){
     int closeset[n], //记录不在S中的顶点在S中的最近邻接点
         lowcost[n], //记录不在S中的顶点到S的最短距离,即到最近邻接点的权值 
         used[n]; //标记顶点是否被访问,访问过的顶点标记为1 

     for (int i = 0; i < n; i++)
     {
        //初始化,S中只有第1个点(0)
        lowcost[i] = G[0][i]; //获取其他顶点到第1个点(0)的距离,不直接相邻的顶点距离为无穷大 
        closeset[i] = 0; //初始情况下所有点的最近邻接点都为第1个点(0) 
        used[i] = 0; //初始情况下所有点都没有被访问过
     }

     used[0] = 1;  //访问第1个点(0),将第1个点加到S中

     //每一次循环找出一个到S距离最近的顶点 
     for (int i = 1; i < n; i++)
     {
         int j = 0;

         //每一次循环计算所有没有使用的顶点到当前S的距离,得到在没有使用的顶点中到S的最短距离以及顶点号 
         for (int k = 0; k < n; k++)
             if ((!used[k]) && (lowcost[k] < lowcost[j])) j = k; //如果顶点k没有被使用,且到S的距离小于j到S的距离,将k赋给j 

         printf("%d %d %d\n",closeset[j] + 1, j + 1, lowcost[j]);   //输出S中与j最近邻点,j,以及它们之间的距离

         used[j] = 1; //将j增加到S中

         //每一次循环用于在j加入S后,重新计算不在S中的顶点到S的距离 
         //主要是修改与j相邻的边到S的距离,修改lowcost和closeset 
         for (int k = 0; k < n; k++)
         {
             if ((!used[k]) && (G[j][k] < lowcost[k]))  //松弛操作,如果k没有被使用,且k到j的距离比原来k到S的距离小 
             { 
                     lowcost[k] = G[j][k]; //将k到j的距离作为新的k到S之间的距离 
                     closeset[k] = j; //将j作为k在S中的最近邻点 
             }
         }
     }
}  

int main(){  
    int a , b , w;  
    scanf("%d%d" , &n , &m);  
    init();  
    for(int i = 0 ; i < m ; i++){  
        scanf("%d%d%d" , &a , &b , &w);  
        if(G[a-1][b-1] > w)  
          G[a-1][b-1] = G[b-1][a-1] = w;  //无向图赋权值 
    }  
    prim();
    system("pause");  
    return 0;  
} 
Kruskal算法

思想:
(1) 将边按权值从小到大排序后逐个判断,如果当前的边加入以后不会产生环,那么就把当前边作为生成树的一条边
(2) 最终得到的结果就是最小生成树

#include <iostream>
#include <algorithm>
using namespace std;

/* 定义边(x,y),权为w */
struct edge
{
    int x, y;
    int w;
};

const int MAX = 26;
edge e[MAX * MAX];
int rank[MAX];/* rank[x]表示x的秩 */
int father[MAX];/* father[x]表示x的父节点 */
int sum; /*存储最小生成树的总权重 */ 

/* 比较函数,按权值非降序排序 */
bool cmp(const edge a, const edge b)
{
     return a.w < b.w;
}

/* 初始化集合 */
void make_set(int x)
{
    father[x] = x;
    rank[x] = 0;
}

/* 查找x元素所在的集合,回溯时压缩路径 */
int find_set(int x)
{
    if (x != father[x])
    {
        father[x] = find_set(father[x]);
    }
    return father[x];
}

/* 合并x,y所在的集合 */
int union_set(int x, int y, int w)
{
    if (x == y) return 0;
    if (rank[x] > rank[y])
    {
        father[y] = x;
    }
    else
    {
        if (rank[x] == rank[y])
        {
            rank[y]++;
        }
        father[x] = y;
    }
    sum += w; //记录权重
    return 1;
}

int main()
{
    int i, j, k, m, n, t;
    char ch;
    while(cin >> m && m != 0)
    {
        k = 0;
        for (i = 0; i < m; i++) make_set(i); //初始化集合,m为顶点个数 

        //对后m-1进行逐行处理 
        for (i = 0; i < m - 1; i++)
        {
            cin >> ch >> n; //获取字符(顶点) 
            for (j = 0; j < n; j++)
            {
                cin >> ch >> e[k].w; //获取权重 
                e[k].x = i;
                e[k].y = ch - 'A';
                k++;
            }
        }

        sort(e, e + k, cmp); //STL中的函数,直接对数组进行排序 

        sum = 0;

        for (i = 0; i < k; i++)
        {
            int result = union_set(find_set(e[i].x), find_set(e[i].y), e[i].w);
            if(result) cout<< e[i].x + 1<< "," << e[i].y + 1 <<endl;
        }

        cout << sum << endl;
    }
    system("pause");
    return 0;
}

5.拓扑排序

  • AOV网
    这里写图片描述

  • 拓扑排序
    (1) 将所有入度为0的点加入队列
    (2) 每次取出队首顶点
    (3) 删除其连出的边,检查是否有新的入度为0的顶点,有则加入队列
    (4) 重复(2)直到队列为空

6.关键路径

  • AOE网
    在AOV网中把边加上权值就变成了AOE网
    这里写图片描述

概念:

  • 顶点 j 事件的最早发生时间,即从源点到顶点 j 的最长路径长度,记作Ve( j );
  • 活动ai的最早发生时间:Ve( j )是以顶点为 j 为起点的出边所表示的活动ai的最早开始时间,记作e( i )
  • 顶点 j 事件的最迟发生时间:即在不推迟整个工程完成的前提下,事件 j 允许最迟的发生时间,记作Vl( j );
  • 活动ai的最迟发生时间:Vl( j ) - (ai所需的时间),就是活动ai的最迟开始时间,其中j是活动ai的终点,记作l(i);


排序算法

1.插入排序

直接插入排序:

  • 每一步把当前的数插入到已经有序的序列中

Shell排序:也称缩小增量排序

  • 根据步长d,把相距间隔为d的元素分到一组,在内部进行直接插入排序,然后步长减半,重复这一步操作,直到步长d=1为止

2.选择排序

简单选择排序:

  • 每一步查找剩余序列中最小的元素,然后将该元素放到已序序列的末尾

堆排序:

  • 还没搞明白

3.交换排序

冒泡排序:

  • 从后往前每一步对比相邻的两个元素,如果后面的元素小,则交换

快速排序:

  • 运用分治思想。每次选择第一个元素作为基准,将比它小的元素放前面,比它大的放后面,接着在这两个子区间继续进行这一操作。

4.归并排序

  • 首先把元素两个一组分组,使每个组内都有序,接下来在把每两组合并,重复这一操作直到只剩一组。

5.基数排序

  • 根据元素的每一位来排序,高位的优先级比低位的高


哈希表

Hash表示一种十分实用的查找技术,具有极高的查找效率

1.Hash函数的构造

没有特定的要求,所以方法很多,只要能尽量避免冲突,就叫好的Hash函数,要根据实际情况来构造合理的Hash函数

  • 直接定址法
    H(key)=keyH(key) = a*key+b

  • 除余法
    以关键码除以表元素总数后得到的余数为地址

  • 基数转换法
    将关键码看作是某个基数制上的整数,然后将其转换为另一基数制上的数

  • 平方取中法
    取关键码的平方值,根据表长度取中间的几位数作为散列函数值

  • 折叠法
    将关键码分成多段,左边的段向右折,右边的段向左折,然后叠加

  • 移位法
    将关键码分为多段,左边的段右移,右边的段左移,然后叠加

  • 随机数法
    选择一个随机函数,取关键码的随机函数值

2.冲突处理方法

开放地址法
  • 线性探查法:冲突后直接向下线性地址找一个新的空间存放
  • 双散列函数法:用两个散列函数来解决冲突
拉链法

将散列表的每个结点增加一个指针字段,用于链接同义词的字表


查找算法

1.顺序查找

从一端开始逐个对比当前结点和关键字是否相等

2.二分查找

要求待查序列为有序表
每次对比中点和关键字是否相等,若相等则找到。若关键字大,则在右边的区间继续这一操作,否则在左边的区间继续这一操作

3.分块查找

用索引表记录块的最大关键字和起始地址,然后查找的时候只要找到关键字所在的块,然后在对应的块中查找就可以了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值