• 第二章 数据结构(一)


    整体结构

    1. 链表与邻接表(用数组模拟)
    2. 栈与队列(用数组模拟)
    3. kmp

    背还是要背的,考试的时候再想是来不及的

    熟练掌握:非常快地将代码默写出来,像背古诗背单词一样

    应试教育:

    记忆力+毅力/自制力

    沉下心来好好背东西

    为什么用数组

    结构体加指针

    struct Node
    {
        int val;
        Node *next;
    }//不讲!面试比较多,笔试比较少
    new Node();//非常慢,笔试的话10000或者1000000,笔试不采用动态链表方式
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    链表与邻接表

    用数组模拟

    1. 单链表:

      • 邻接表 其实是n个链表,最主要应用是存储图和树
    2. 双链表

      • 优化某些问题

    单链表

    存储

    image-20220621095657429

    image-20220621100009758
    $$

    $$
    每个点有两个值,e与ne通过下标相互关联,空节点的下标用-1表示

    插入

    单链表只向后看,不往前看。

    插入至头结点

    image-20220731171517553

    先处理插入值的e,ne;之后处理前驱head与idx

    将x插入到下标为k的点后面

    image-20220731172021541

    先处理插入值的e,ne;之后处理前驱ne[k]与idx

    删除

    image-20220731172122695

    通过ne[k]找到后继,通过ne[ne[k]]找到后继的后继

    遍历

    从下标为head一直到-1

    关于链表的删除:

    算法题不是写工程,写工程为动态链表,需要考虑空间释放与内存泄漏

    算法题不需要负责,浪费就浪费,只需要保证程序在1s内执行完毕

    #include 
    using namespace std;
    const int N = 100010;
    
    //head 表示头结点的下标,所有节点都可以用下标来索引
    //e[i] 表示节点i的值
    //ne[i] 表示节点i的next指针是多少,节点i的下一个坐标在什么地方
    //idx 指针,从第一个点开始,存储当前已经用到的哪个地址。
    //当需要分配新的点的时候,将idx指向的节点分配,idx向后移动一位
    int head,e[N],ne[N],idx;
    
    
    //初始化
    void init()
    {
       head  = -1;  //空的
       idx = 0;
    }
    
    //将x插入头结点,算法题中80%操作是把节点插入至头结点
    void add_to_head(int x)
    {
        //分配节点
        e[idx] = x,ne[idx] = head;
        head = idx;
        idx++;
    }
    
    //将x插入到下标为k的点后面
    void insert(int x,int k)
    {
        e[idex] = x,ne[idex] = ne[k],ne[k] = idx,idx++;
    }
    
    //将下标是k的点后面的点删掉
    void remove(int k)
    {
        ne[k] = ne[ne[k]];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    具体题目

    //826
    实现一个单链表,链表初始为空,支持三种操作:
    
    向链表头插入一个数;
    删除第 k 个插入的数后面的数;
    在第 k 个插入的数后插入一个数。
    现在要对该链表进行 M 次操作,进行完所有操作后,从头到尾输出整个链表。
    
    注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。
    
    输入格式
    第一行包含整数 M,表示操作次数。
    
    接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:
    
    H x,表示向链表头插入一个数 x。
    D k,表示删除第 k 个插入的数后面的数(当 k 为 0 时,表示删除头结点)。
    I k x,表示在第 k 个插入的数后面插入一个数 x(此操作中 k 均大于 0)。
    输出格式
    共一行,将整个链表从头到尾输出。
    
    数据范围
    1≤M≤100000
    所有操作保证合法。
    
    输入样例:
    10
    H 9
    I 1 1
    D 1
    D 0
    H 6
    I 3 6
    I 4 5
    I 4 5
    I 3 4
    D 6
    输出样例:
    6 4 6 5
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    #include 
    using namespace std;
    const int N = 100010;
    
    //head 表示头结点的下标,注意是下标
    //e[i] 表示节点i的值
    //ne[i] 表示节点i的next指针是多少
    //idx 存储当前已经用到的哪个点
    int head,e[N],ne[N],idx;
    
    
    //初始化
    void init()
    {
       head  = -1;
       idx = 0;
    }
    
    //将x插入头结点
    void add_to_head(int x)
    {
        //处理e,ne数组
        e[idx] = x,ne[idx] = head;
        //处理idx
        head = idx;
        //最后idx移位
        idx++;
    }
    
    //将x插入到下标为k的点后面
    void add(int k,int x)
    {
        e[idx] = x,ne[idx] = ne[k],ne[k] = idx,idx++;
    }
    
    //将下标是k的点后面的点删掉
    void remove(int k)
    {
        ne[k] = ne[ne[k]];
    }
    
    
    int main()
    {
        int m;
        cin>>m;
        init();
        while(m--)
        {
            int k,x;
            char op;
            cin>>op;
            if(op == 'H')
            {
                cin>>x;
                add_to_head(x);
            }
            else if(op == 'D')
            {
                cin>>k;
                //特判 k == 0
                if(!k) head = ne[head];
                else remove(k-1);
            }
            else
            {
                cin>>k>>x;
                add(k-1,x);
            }
        }
        //遍历链表,注意终止条件是到达链表尾部,即下标变为-1(初始化时head = -1,-1代表尾节点下标,随着不断插入,-1一直向后移动)
        for(int i = head;i!=-1;i = ne[i]) cout<<e[i]<<' ';
        cout<<endl;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75

    双链表

    让下标为0的点为头结点,下标为1的点为尾节点

    image-20220731173215142

    初始化

    image-20220731173402601

    为方便,下标为0为头部,下标为1为尾部,任意节点最终都插入至两者之间,因此不需要head指针

    插入

    在在下标为k的右边插入一个点

    image-20220731173634967

    在下标为k的左边插入一个点 add(l[k],x)

    为避免出错,先处理左指针再处理右指针,先左后右。

    删除

    image-20220731174635083

    #include 
    
    using namespace std;
    
    const int N = 100010;
    
    int m;
    
    int e[N],l[N],r[N],idx;
    /*
    struct Node
    {
        int l,r,e;
    }nodes[N]
    */
    //初始化
    void init()
    {
        //0表示左端点,1表示右端点
        r[0] = 1,l[1] = 0;
        idx = 2;
    }
    //在下标为k的右边插入一个点
    void add(int k,int x)
    {
        e[idx] = x;
    
        l[idx] = k;
        r[idx] = r[k];
        l[r[k]] = idx;
        r[k] = idx;   
        idx++;
    }
    
    //删除第k个点
    void remove(int k)
    {
    
        l[r[k]] = l[k];
        r[l[k]] = r[k];
        //如果使用结构体
        //nodes[nodes[k].l].r = nodes[k].r;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    邻接表

    邻接表就是一堆单链表

    image-20220623083544913

    栈和队列

    image-20220731175404928

    特点:头插头出

    image-20220808091647961

    #include 
    
    using namespace std;
    
    const int N = 100010;
    //*******************************栈
    //tt 栈顶下标 tt = 0 ,stk[0]不存放数据
    int stk[N],tt;
    
    //插入
    stk[++tt] = x;
    //删除
    tt--//判断栈是不是为空
    if(tt>0) not empty
    else empty
    
    //栈顶
    stk[tt]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    队列

    image-20220731175909946

    特点:

    尾插头出

    image-20220808092020705

    //******************************** 队列
    //hh 队头,tt 队尾
    //在队尾插入元素,在队头弹出元素
    int q[N],hh,tt = -1;
    //插入
    q[++tt] = x;
    
    //从队头弹出
    hh++;
    //从队尾弹出
    tt--;
    
    //判断是否为空
    if(hh <= tt) not empty
    else empty
    
    //取出队头元素
    q[hh]
    //取队尾元素
    q[tt]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    单调栈

    给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。
    
    输入格式
    第一行包含整数 N,表示数列长度。
    
    第二行包含 N 个整数,表示整数数列。
    
    输出格式
    共一行,包含 N 个整数,其中第 i 个数表示第 i 个数的左边第一个比它小的数,如果不存在则输出 −1。
    
    数据范围
    1≤N≤105
    1≤数列中元素≤109
    输入样例:
    5
    3 4 2 7 5
    输出样例:
    -1 3 -1 2 2
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    image-20220731180424025

    思路:先想一下暴力做法是什么,再挖掘一些性质,使我们可以将目标集中在比较少的状态中,从而将问题的复杂度降低

    暴力思路

    image-20220623091326958

    性质:i向右移动过程中,用一个栈存储i左边的所有元素

    image-20220731180907608

    栈里面是不是有些元素永远不会输出来

    image-20220731181050487

    举例:

    image-20220731180424025

    数字2在4的后面输入,2比4小并且在4的后面,因此4永远不会被输出。在单调栈中应该提前弹出。

    删除不会输出的点,剩余点的格式严格单调上升

    image-20220731181125455

    image-20220731181304961

    时间复杂度:每个元素只会进栈一次,每个元素只会出栈一次。所以最多2n次操作。

    不会从头操作,整个算法的复杂度为o(n)

    #include 
    
    using namespace std;
    
    const int N = 100010;
    
    int n;
    int stk[N],tt;
    
    int main()
    {
        // ios::sync_with_stdio(false);
        // cin >> n;
        scanf("%d",&n);
        for(int i = 0;i<n;i++)
        {
            int x;
            // cin>>x;
            scanf("%d",&x);
            //栈中如果存在数据while(tt)并且栈中数据比x大,进行出栈,每个数据执行这个过程
            while(tt && stk[tt] >= x) tt--;
            // if(tt) cout<
            //如果栈中存在数据,打印栈顶元素
            if(tt) printf("%d ",stk[tt]);
            else printf("-1 ");
            // else cout <<-1<<" ";
            
            //进栈
            stk[++tt] = x;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    简洁版

    #include 
    
    using namespace std;
    
    const int N = 100010;
    
    int stk[N],tt = 0;
    
    int main()
    {
        int n,x;
        scanf("%d",&n);
        for(int i = 0;i < n;i++)
        {
            scanf("%d", &x);
            while(tt && stk[tt] >= x) tt--;
            if(tt) printf("%d ",stk[tt]);
            else printf("-1 ");
            stk[++tt] = x;
        }
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    单调队列

    最常见题型就几道。最典型问题

    思路相同:

    先想一下暴力做法怎么做,观察其中没有用的元素,把其中没有用的元素删掉得到单调性。在看有没有单调性。有单调性的话再去优化问题

    给定一个大小为 n≤106 的数组。
    
    有一个大小为 k 的滑动窗口,它从数组的最左边移动到最右边。
    
    你只能在窗口中看到 k 个数字。
    
    每次滑动窗口向右移动一个位置。
    
    以下是一个例子:
    
    该数组为 [1 3 -1 -3 5 3 6 7],k 为 3。
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    image-20220806104857789

    你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。
    
    输入格式
    输入包含两行。
    
    第一行包含两个整数 n 和 k,分别代表数组长度和滑动窗口的长度。
    
    第二行有 n 个整数,代表数组的具体数值。
    
    同行数据之间用空格隔开。
    
    输出格式
    输出包含两个。
    
    第一行输出,从左至右,每个位置滑动窗口中的最小值。
    
    第二行输出,从左至右,每个位置滑动窗口中的最大值。
    
    输入样例:
    8 3
    1 3 -1 -3 5 3 6 7
    输出样例:
    -1 -3 -3 -3 3 3
    3 3 5 5 6 7
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    暴力求解方法 O(nk)

    栈里面存储元素的下标窗口由队列来维护,保证队列中时时刻刻保存当前窗口的元素

    栈里面存储元素的下标

    步骤:

    1. 在队尾插入1、3、-1
    image-20220806113638023
    1. 第四次:先在队尾插入-3,然后在队头弹出1,窗口由队列来维护,保证队列中时时刻刻保存当前窗口的元素

    image-20220806113803592

    image-20220801095148114

    1. 优化:-3进来以后,3、-1永远不会输出。前面值比后面值大,前面的数值一定不会弹出来。因此可以将前面数值较大的点删除。整个队列成为严格上升的趋势。一个严格上升的最小值在队头。每次找最小值,找队头就可以了

      3 、-1会被弹出。3是头,-3是尾巴

    新插入的值比队尾大的时候,将队尾删除

    image-20220801100019746

    1. 如何判断窗口长度是否溢出:

    队列中存储的数组下标而不是存储数据值,因为如果存储数据值,则无法进行判断到底是否溢出。存储下标的话,只需要套一层,就可以得到对应的数据值。

    i–当前队列的右端点。k–窗口长度

    队列中存储的不是值,而是数组下标。

    队头的下标是否超出了i-k+1,i,如果超出,弹出队头

    #include 
    
    using namespace std;
    
    const int N = 1000010;
    
    int n,k;
    //a[N]val q[N]队列中存储下标,想要取值的时候还需要套一层
    int a[N],q[N];
    int main()
    {
        scanf("%d%d",&n,&k);
        for(int i  =0 ;i<n;i++) scanf("%d",&a[i]);
        int hh = 0,tt = -1;
        for(int i = 0;i<n;i++)
        {
            //1. 判断队头是否已经划出窗口--判断是不是空的并且当前是i。
            //由于遍历,每次最多只有一个数不在窗口内,写if足够使用
            //q[hh]当前头部下标 i-k+1 窗口的最左端
            if(hh<=tt && q[hh] < i-k+1) hh++;
            //2. 如果新插入的数比队尾小,则队尾不起作用,删除队尾直到小于新插入的数为止
            while(hh<=tt && a[q[tt]]>=a[i]) tt--;
            //3. 在队尾插入新的数组下标索引
            q[++tt] = i;
            //4. 当i不足k时,不用输出
            if(i>=k-1) printf("%d ",a[q[hh]]);
     
        }
        puts("");
        hh = 0,tt = -1;
        for(int i = 0;i<n;i++)
        {
            //判断队头是否已经划出窗口
            if(hh<=tt && i-k+1 > q[hh]) hh++;
            while(hh<=tt && a[q[tt]]<=a[i]) tt--;
            q[++tt] = i;
            if(i>=k-1) printf("%d ",a[q[hh]]);
     
        }
        puts("");
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    KMP

    给定一个字符串 S,以及一个模式串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
    
    模式串 P 在字符串 S 中多次作为子串出现。
    
    求出模式串 P 在字符串 S 中所有出现的位置的起始下标。
    
    输入格式
    第一行输入整数 N,表示字符串 P 的长度。
    
    第二行输入字符串 P。
    
    第三行输入整数 M,表示字符串 S 的长度。
    
    第四行输入字符串 S。
    
    输出格式
    共一行,输出所有出现位置的起始下标(下标从 0 开始计数),整数之间用空格隔开。
    
    数据范围
    1≤N≤105
    1≤M≤106
    输入样例:
    3
    aba
    5
    ababa
    输出样例:
    0 2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    用朴素算法解决KMP问题

    用图表示匹配过程,红绿圈之前的字符串均相同。暴力做法移动一位之后再进行匹配

    image-20220801101142848

    假定移动一定长度后可以重新匹配,即下图所示的情况。蓝色表示的为同一块区域

    image-20220806151935003

    则存在蓝色与棕色相等的关系。因此可以对模板串进行预处理。需要对每一个点都预处理。即后缀与前缀相等。相等的最大长度是多少。

    image-20220806152048006

    next数组含义:

    以i为终点的后缀与一开始的前缀相等并且后缀的长度最长

    如下图表示

    next[i] = j即:p[1, j] = p[p-j+1,i]

    image-20220806153612998

    最多向后移动多长距离只和模板串有关系–后缀与前缀相等的最大长度是多少

    image-20220801101428248

    以最终对比为例,移动后点不同点变为next[j],再对比后面的值

    image-20220801101815888

    求next数组的过程

    image-20220806155630965

    #include 
    using namespace std;
    
    const int N = 100010,M = 1000010;
    
    int n,m;
    
    char p[N],s[M];
    //next数组在某些头文件中用过,用的时候可能报错,因此替换为ne
    int ne[N];
    
    int main()
    {
        //数组下标从1开始
        cin>>n >> p + 1 >> m >> s + 1;
        
        //求next过程
        //i是从2开始,ne[1] = 0:因为如果ne[1]失败后,只能从0开始,因此不必要计算
        for(int i = 2,j = 0;i<=n;i++)
        {
            //
            while(j && p[i] != p[j+1]) j = ne[j];
            if(p[i] == p[j+1]) j++;
            ne[i] = j;
        }
        //KMP匹配过程
        //i是遍历所有数组,j是从0开始做
        for(int i = 1,j = 0;i <= m;i++)
        {
            //j没有退回起点并且当前s[i]与p[j+1]不同,s[i]不能与我下一个j去匹配
            //直到j退到开头,退无可退或者可以匹配了
            //判断j是否退无可退与是否j可以向后走
            //如果最终真的退无可退了,直接对比原始串的下一个字符即i++
            while(j && s[i] != p[j+1]) j = ne[j];
            //如果两者匹配了,j移动到下一位置j++
            if(s[i] == p[j+1]) j++;
            if(j == n)
            {
                //匹配成功
                printf("%d ",i-n);
                //完全匹配成功后,向后移动的下标
                j = ne[j];
            }
            
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    举例:

    image-20220801104518241

    P的next数组:前缀与后缀相等的最大长度

    next下标数值
    10
    20
    31
    42
    53
    64
        for(int i = 2,j = 0;i<=n;i++)
        {
            while(j && p[i] != p[j+1]) j = ne[j];
            if(p[i] == p[j+1]) j++;
            ne[i] = j;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
  • 相关阅读:
    一分钟学一个 Linux 命令 - mv 和 cp
    Python教程
    绝绝子还是YYDS,2021国民年度流行语出炉
    项目工作中,管理者如何合理安排任务优先级?
    Java反射实体组装SQL
    安防行业集团采购管理系统:全程数字化采购执行,规范化企业采购业务流程
    Zookeeper学习笔记(1)—— 基础知识
    并查集讲解
    计算机三级数据库高级查询
    使用virtualbox借助Vagrant快速创建Linux虚拟机
  • 原文地址:https://blog.csdn.net/m0_49448331/article/details/126221810