线性表详解

👂 答案 - 杨坤/郭采洁 - 单曲 - 网易云音乐

👂 多年后再见你 - 乔洋/周林枫 - 单曲 - 网易云音乐

目录

🌳3.1  顺序表

(1)插入

(2)删除

(3)总结

🌳3.2  单链表

(1)插入

(2)删除

🌳3.3  双向链表

(1)插入

(2)删除

🌳3.4  双向链表

🌳3.5  静态链表

(1)单链表

(2)双向链表

🌼例题1  The Blocks Problem

🌼例题2  Broken Keyboard (a.k.a. Beiju Text)

🌼例题3  Boxes in a Line

思路

操作代码

坑 

AC  代码 

🥇总结


🌳3.1  顺序表

(1)插入

位置 i 插入元素e,需要将第L.length至第 i 个元素,依次后移一个位置,空出第 i 个位置

比如第5个位置插入元素9

注意,i 表示的是实际的第几个元素(从1开始),而 [] 中的是索引(从0开始)

bool ListInsert_Sq(SqList &L, int i, int e)
{
    if(i < 1 || i > L.length + 1) return false; //插入为止i不合法
    if(L.length == Maxsize) return false; //存储空间满了
    for(int j = L.length - 1; j >= i - 1; j--) 
        L.elem[j + 1] = L.elem[j]; //从最后一个元素开始后移, 直到第i个元素
    L.elem[i - 1] = e; //新元素e放入第i个位置
    L.length++; //表长+1
    return true;
}

顺序表插入元素算法,平均时间复杂度为O(n)

分析 

\sum pi * (n - i + 1) = \frac{1}{n + 1}\sum (n - i +1) = \frac{1}{n + 1}(n + (n - 1) + ... + 1 + 0) = \frac{n}{2}

上下范围没法打出来,求和上限是n + 1下限是 i = 1

思路

可以在第1个位置插入,第2个...第n + 1个位置插入(这里的位置从1开始)

共有n + 1个可以插入的位置

每种情况移动的元素个数都是 n - i + 1

将每种情况移动次数 * 概率pi,求和,就是平均时间复杂度

所以...每个位置插入概率位 1 / (n + 1),该位置移动次数为 n - i + 1

(2)删除

删除第 i 个元素,需要将该元素暂存到变量e中(后续可能用上),然后从第 i + 1个元素开始前移...直到把第 n 个元素也前移 1 位

比如,删除第5个元素

注意,i 表示的是实际的第几个元素(从1开始),而 [] 中的是索引(从0开始)

bool ListDelete_Sq(SqList &L, int i, int &e)
{
    if(i < 1 || i > L.length) return false; //插入位置不合法
    e = L.elem[i - 1]; //保存要删除的元素
    for(int j = i; j <= L.length - 1; j++)
        L.elem[j - 1] = L.elem[j]; //被删除元素后的元素前移
    L.length--; //表长-1
    return true;
}

顺序表删除元素算法,平均时间复杂度O(n)

分析

删除元素有 n 种情况,所以删除某个元素的概率 pi 为 1 / n,每种情况移动次数都是 n - i

\sum pi * (n - i) = \frac{1}{n}\sum (n - i) = \frac{1}{n} * ((n - 1) + ... + 1 + 0) = \frac{n - 1}{2}

(3)总结

顺序表

优点:存储密度高,只需O(1)时间就可以取出第 i 个元素

缺点:需预先分配最大空间,大了浪费,小了溢出;且插入删除时,需要移动大量元素

需要插入,删除时,往往采用链式存储

🌳3.2  单链表

提高效率,不准备敲理论内容,看这一篇就够了👇

最详细的C++单向链表实现_c++实现单链表_思泽Elly的博客-优快云博客

再给大家个福利,新加坡国立大学数据结构可视化网站👇

链表(单向,双向),堆栈,队列,双端队列 - VisuAlgo

可视化比如这样👇

 

下面是结合gpt注释的书中代码

(1)插入

​
bool ListInsert_L(LinkList &L, int i, int e) //单链表插入, 在第i个节点插入元素e
{
    //在带头节点的单链表L中第i个位置插入值为e的新节点

    int j; //定义计数器
    LinkList p, s; //定义两个链表指针
    p = L; //将p指向链表的头结点,即第一个元素之前的结点,也就是头指针
    j = 0; //初始化计数器为0

    while(p && j < i - 1) { //查找第i-1个节点, p指向该节点
        p = p->next; //指向下一个节点
        j++; //计数器加1
    }

    if(!p || j > i - 1) //i > n+1 或 i < 1
        return false; //返回false

    s = new Lnode; //生成新节点
    s->data = e; //将数据元素e放入新节点的数据域中
    s->next = p->next; //将新节点的指针域指向第i个节点
    p->next = s;//将第i-1个节点的指针域指向新节点

    return true; //返回true
}

​

(2)删除

bool ListDelete_L(LinkList &L, int i) //在带头节点的单链表L中删除第i个元素
{
    LinkList p, q; //定义两个链表指针
    int j; //定义计数器
    p = L; //将p指向链表的头结点,即第一个元素之前的结点,也就是头指针
    j = 0; //初始化计数器为0

    while((p->next) && (j < i - 1)) { //查找第i-1个节点, p指向该节点
        p = p->next; //指向下一个节点
        j++; //计数器加1
    }

    if(!(p->next) || (j > i - 1)) //i > n 或 i < 1
        return false; //返回false

    q = p->next; //保存要删除的节点的地址
    p->next = q->next; //将p的指针域指向q的下一个节点
    delete q; //释放q所占用的空间

    return true; //返回true
}

🌳3.3  双向链表

双向链表,每个节点,包含三个域:数据域和两个指针域

(1)插入

代码中设计3个节点

1,节点p

2,节点p的前驱节点p->prior(p的上一个节点)

3,节点s(要插入的节点)

//在双向链表L的第i个位置插入元素e
bool ListInsert_L(DuLinkList &L, int i, int e)
{
    int j;
    DuLinkList p, s;
    p = L; //p指向链表L
    j = 0;

    //查找第i个节点,p指向该节点
    while(p && j < i) {
        p = p->next; //p后移一个节点
        j++;
    }

    //insert位置不合法,返回false
    if(!p || j > i) 
        return false;

    //生成新节点s
    s = new DuLnode;
    s->data = e; //新节点数据为e

    //更新指针域,将s插入到p和p前一个节点之间
    p->prior->next = s; //p前一个节点的next指针域指向节点s的地址
    s->prior = p->prior; //p前一个节点的地址赋值给节点s的prior指针域
    s->next = p; //节点p的地址赋值给节点s的next指针域
    p->prior = s; //节点s的地址赋值给p的prior指针域

    return true;
}

//其中DuLnode是双向链表的节点结构体,结构如下:
typedef struct DuLnode {
    ElemType data; //数据域
    struct DuLnode *prior, *next; //前驱和后继指针
} DuLnode, *DuLinkList;

从左至右,依次连接

(2)删除

删除一个节点,就是跳过这个节点

p->prior->next = p->next;
p->next->prior = p->prior;

注意跳过节点p后,要delete p,释放被删除节点的空间

还有篇博客,我没细看,感觉双向链表挺简单的,可能有书的原因(6条消息) 双向链表(Double Linked List)_Hacker_徐的博客-优快云博客

//从双向链表L中删除第i个元素
bool ListDelete_L(DuLinkList &L, int i)
{
    DuLinkList p;
    p = L; //p指向链表L
    int j = 0;

    //查找第i个节点,p指向该节点
    while(p && j < i) {
        p = p->next; //p后移一个节点
        j++;
    }

    //i > n 或 i < 1时, 删除位置不合理,返回false
    if(!p || j > i) 
        return false;

    //更新指针域,将p从双向链表中删除
    if(p->next)  //如果p后继节点存在
        p->next->prior = p->prior; //将p的后继节点前驱指针指向p的前驱节点
    p->prior->next = p->next; //将p的前驱节点后继指针指向p的后继节点
    delete p; //释放p的内存

    return true;
}

//其中DuLnode是双向链表的节点结构体,结构如下:
typedef struct DuLnode {
    ElemType data; //数据域
    struct DuLnode *prior, *next; //前驱和后继指针
} DuLnode, *DuLinkList;

注意,赋值给p->next前,需要先判断p->next是否存在,避免访问到空指针

🌳3.4  双向链表

循环链表及其基本操作 - 随手一只风 - 博客园 (cnblogs.com)

单向循环链表最后一个节点的next域不为空,而是指向了头节点

注意,单链表和单向循环链表判断空表的条件也变化了

1,单链表为空时,L->next = NULL;

2,单向循环链表为空时,L->next = L;

3,双向循环链表尾空时,L->next = L->prior = L;

链表优点:动态,不需分配最大空间,插入删除不需移动元素

缺点

动态分配节点,节点地址不连续,需要指针域记录下一个节点地址

而指针域占用一个int空间,存储密度低(数据所占空间 / 节点所占空间)

存取元素需要头到尾顺序查找,属于顺序存取

🌳3.5  静态链表

静态链表位置0不存储数据,为头节点

Google,百度了下,讲的稀巴烂,还不如自己写(主要是,讲的不够直观,看了5秒看不出个所以然)

静态链表,就是数组模拟链表,通过增加1个或2个数组,代替结构体和指针的使用

单链表中,right[]记录每个元素的后继下标

双向链表中,right[]记录后继下标,left[]记录前驱下标

动态链表,就是结构体包含data和*prior,*next三个成员(数据域,前驱,后继指针)

(1)单链表

(1)插入

比如,25插入到第6个元素的位置,只需将25放入data尾部,再修改后继数组👇

data[9] = 25; //插入元素放data[]尾部
right[5] = 9; 
right[9] = 6;

(2)删除

比如,删除第3个元素

right[2] = 4;

注意,第3个元素并没有真正删除,只是不在链表中,被跳过了

(2)双向链表

(1)插入

比如,25插入到第6个元素的位置

data[9] = 25; //插入元素放data[]尾部
right[5] = 9; //后面4行依次修改
left[9] = 5;
right[9] = 6;
left[6] = 9;

(2)删除

比如,删除第3个元素

right[2] = 4;
left[4] = 2;

🌼例题1  The Blocks Problem

The Blocks Problem - UVA 101 - Virtual Judge (vjudge.net)

英文题目中,前2段都是故事背景,没必要看,从第3段开始看就行(第3段有 n, i, bi, bi+1等变量)

没有使用链表,用的vector代替二维数组,也利用了栈后进先出的特性(新元素存放在栈顶) 

本题在读懂题意基础上,我们需要通过2个变量,来定位一个元素,1是高度h,2是下标

坑 1

solve()函数中读入一行时,👇下面是错误的

这样当quit输入后,还需要多输入3个变量才能输出

for(int i = 0; i < n; ++i) {
        cin>>s1>>a>>s2>>b; //输入
        if(s1 == "quit") break;
...
}

坑 2

The input begins with an integer n on a line by itself representing the number of blocks in the block
world. You may assume that 0 < n < 25.

翻译

输入以一个整数n开始,它本身在一行中代表块中的块数
世界中的块数。你可以假设0 < n < 25

意味着,n只是表示init()初始化时,一共有0~n-1共n个不同位置,而不是说它会输入n行

Your program should process all commands until the quit command is encountered. 

👆这里说的很清楚了,知道遇到"quit"命令

AC  代码

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

vector<int>block[30]; //2维vector
int n;

void init()
{
    cin>>n;
    for(int i = 0; i < n; ++i)
        block[i].push_back(i);
}

//归位, 位置和高度
void goback(int p, int h) //p堆 > h 的所有元素归位
{
    for(int i = h + 1; i < block[p].size(); ++i) { //高度h以上的归位
        int k = block[p][i]; //具体元素
        block[k].push_back(k); //下标k插入k
    }
    block[p].resize(h + 1); //0 ~ h共h + 1个元素, 重置大小
}

//查找元素x所在位置
void loc(int x, int &p, int &h) //将位置和高度传回去
{
    for(int i = 0; i < n; ++i)
        for(int j = 0; j < block[i].size(); ++j)
            if(block[i][j] == x) {
                p = i, h = j;
                return;
            }
}

void move_all(int h, int p, int q) //p下标高度 >= h 的部分移动到 下标q上
{
    for(int i = h; i < block[p].size(); ++i) {
        int k = block[p][i];
        block[q].push_back(k);
    }
    block[p].resize(h); //重置block[p]大小
}

void solve()
{
    int a, b; string s1, s2;
    while(cin>>s1) {
        if(s1 == "quit") return;
        cin>>a>>s2>>b;

        int p1 = 0, h1 = 0, p2 = 0, h2 = 0;
        loc(a, p1, h1); //得到元素a位置和高度
        loc(b, p2, h2);

        if(p1 == p2) continue; //非法命名, 不予执行
        if(s1 == "move") goback(p1, h1);
        if(s2 == "onto") goback(p2, h2); //归位
        //归位后整体移动
        move_all(h1, p1, p2);
    }
}

void print()
{
    for(int i = 0; i < n; ++i) {
        cout<<i<<":";
        for(int j = 0; j < block[i].size(); ++j)
            cout<<" "<<block[i][j];
        cout<<endl;
    }
}

int main()
{
    init(); //初始化
    solve(); //输入和处理
    print(); //输出

    return 0;
}

🌼例题2  Broken Keyboard (a.k.a. Beiju Text)

Broken Keyboard (a.k.a. Beiju Text) - UVA 11988 - Virtual Judge (vjudge.net)

翻译

 Each test case is a single line containing at least one and at most 100,000 letters, underscores and two special characters ‘[’ and ‘]’. ‘[’ means the “Home” key is pressed internally, and ‘]’ means the “End” key is pressed internally. The input is terminated by end-of-file (EOF)

每个测试用例是一个单行,至少1个最多100,000个,字母、下划线或特殊字符'[' ,']'
']'意味着内部按了 "Home "键,而']'意味着内部按了 "End "键。输入由文件末尾的
(EOF) 

解释

遇到 '[' 光标会跳到开头,遇到 ']' 光标会跳到结尾 

需要在头尾进行操作,用顺序表,需要移动大量元素,所以我们用STL的list双向链表,速度较快

涉及了

1,list<char>text;的声明,即每个元素是char型的

2,迭代器以及begin(), end()两个指针的使用

3,insert()函数

1,每次插入后,不要漏it++;  同时it++;应该放在else的大括号内

2,迭代器 it 记得初始化(常犯错误)

困惑

搞不懂为什么,后面已经有it++了,前面还一定要用 it 接收插入的位置呢....

it = text.insert(it, s[i]);
it++;

gpt4.0的解释

在C++中,list 是一个双向链表容器,每个元素都是通过链表节点连接在一起。迭代器 (iterator) 是用于访问和操作容器元素的对象。

当我们调用 text.insert(it, s[i]) 时,它会将 s[i] 插入到 it 所指向的位置之前,并返回一个指向插入的元素的迭代器。在这种情况下,为了保持迭代器的正确性,我们将返回的迭代器赋值给 it

由于插入操作可能导致链表结构的改变,因此原始的迭代器 it 可能会失效或者指向错误的位置。通过将返回的迭代器赋值给 it,我们确保 it 始终指向当前正确的位置。

然后,通过 it++,我们将迭代器 it 向后移动一个位置,使其指向下一个元素。这样,在下一次迭代时,我们可以继续将新的元素插入到正确的位置。

所以,text.insert(it, s[i]) 等价于 it = text.insert(it, s[i]); it++;,这样可以确保插入操作的正确性,并更新迭代器的位置。

记忆

text.insert(it, s[i]);
//等价于
it = text.insert(it, s[i]);
it++;

AC  代码

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

void solve(string s)
{
    list<char>text;
    list<char>::iterator it = text.begin();
    for(int i = 0; i < s.length(); ++i) { //读入
        if(s[i] == '[') it = text.begin();
        else if(s[i] == ']') it = text.end();
        else {
            it = text.insert(it, s[i]);
            it++; //这里漏了, 插入位置会错
        }
    }
    //输出
    for(it = text.begin(); it != text.end(); ++it)
        cout<<*it;
    cout<<endl;
    s.clear();
}

int main()
{
    string s;
    while(cin>>s)
        solve(s);

    return 0;
}

🌼例题3  Boxes in a Line

Boxes in a Line - UVA 12657 - Virtual Judge (vjudge.net)

翻译

the sum of numbers at odd-indexed positions. Positions are numbered 1 to n from left to right.

奇数索引位置的数字之和。从左到右为1~n的数字

本题,就双向静态链表来说,除了建2个数组外(由于存储的数据按1~n,所以不需要存储数组的数组,只需要前驱和后继),只有2个封装好的函数,一个是init()初始化,一个是link()链接,一个链接就能处理删除,插入,交换,逆序4种操作

当然,需要考虑(分类讨论)较为全面

思路

由题目,移动需要删除和插入2个操作,还需要查找元素的位置,但是查找是链表不擅长的,链表查找的时间复杂度为O(n),而由题,n <= 1e5,多次查找会超时

所以我们采取静态链表实现(快速插入删除 + 快速查找),因为题目包含向后和向前操作,我们采取静态双向链表。

同时,由于链表中含大量元素,翻转操作复杂度高,只需做标记即可,不用真的翻转

算法设计

(1)初始化双向静态链表(前驱数组l[],后继数组r[],翻转标记flag = false)

(2)读入操作指令a

(3)a = 4则标记翻转,flag = !flag,否则读入x, y

(4)如果 a != 3 && flag,则a = 3 - a,因为此时左右是倒置的,2表示1,1表示2

(5)对于1,2指令,本来位置对的话,不予操作

(6)a = 1,删除x,且x插入y左侧

(7)a = 2,删除x,且x插入y右侧

(8)a = 3,分为相邻和不相邻两种情况

操作代码

1,链接

void link(int L, int R) 
{
    r[L] = R; //L的后继为R
    l[R] = L; //R的前驱为L
} //L和R链接起来, 类似 L -- R

2,删除

//删除x, 只需将x跳过去
link(Lx, Rx); //将x的前驱和后继链接

3,插入

x插入y左侧:先删除x,再将x插入

//删除需要1次链接
//插入需要2次链接
link(Lx, Rx); //删除x
link(Ly, x); //y前驱节点与x链接
link(x, y); //x和y链接

4,插入

x插入y右侧

link(Lx, Rx);
link(y, x); //y和x --
link(x, Ry); //x与y后继节点 --

上面的是相邻,下面的是不相邻

相邻只需要link三次,不相邻需要link四次,都不需要删除

5,交换(相邻)

//做题时建议快速画出草图, 便于理解
link(Lx, y); 
link(y, x);
link(x, Ry);

相邻需要考虑X,Y前后的分类讨论

6,交换(不相邻)

link(Lx, y);
link(y, Rx);
link(Ly, x);
link(x, Ry);

不相邻不需要分类讨论(看上图👆) 

7,翻转

如果标记了翻转,flag == true

1,长度n为奇数,正向与反向一样,不用操作

2,长度n为偶数,反向奇数位的和 = 所有元素和 - 正向奇数位的和

所以,只需统计正向奇数位的和,再判断1,flag == true   2,长度 n 是否为偶数

👇需要注意的是,静态双向链表初始化时,习惯性初始化为循环链表,头节点和尾节点连在一起

且头节点不存储数据

第2个需要注意的点:统计和的时候,要根据l[]和r[],而不是从头到尾按顺序

坑 

因为一个没保存初始值,花了2小时找bug

int lx = l[x], ly = l[y], rx = r[x], ry = r[y]; //link操作会改变同一个操作中的初始值

一开始觉得不用保存初始值,是因为,觉得同一步操作不会影响,实际上,在3步链接或者4步链接中,当y的左端变成lx后,ly就不是ly了,而是lx,自然影响到下一行代码的执行 

AC  代码 

#include<iostream>
using namespace std;
int l[100010], r[100010]; //前驱 后继数组

void init(int n)
{
    for(int i = 1; i < n; ++i) {
        l[i] = i - 1;
        r[i] = i + 1;
    }
    l[0] = n, r[0] = 1; //循环
    l[n] = n - 1, r[n] = 0;
}

void link(int L, int R) //L与R连接
{
    r[L] = R;
    l[R] = L;
}

int main()
{
    int n, m, a, x, y, number = 1; //Case number

    while(cin>>n>>m) { //n个数据 m行命令

        bool flag = false; //未翻转
        init(n); //初始化前驱 后继数组

        for(int i = 0; i < m; ++i) {
            cin>>a;
            if(a == 4) flag = !flag; //标记要翻转
            else {
                cin>>x>>y;
                if(a != 3 && flag)
                    a = 3 - a; //命令1和2交换
                if(a == 1 && l[y] == x)
                    continue; //不予操作
                if(a == 2 && r[y] == x)
                    continue;
                //再对1 2 3分类讨论
                int lx = l[x], ly = l[y], rx = r[x], ry = r[y]; //link操作会改变同一个操作中的初始值
                if(a == 1) {
                    link(lx, rx); //删除x
                    link(ly, x);
                    link(x, y);
                }
                if(a == 2) {
                    link(lx, rx);
                    link(y, x);
                    link(x, ry);
                }
                if(a == 3) {
                    //相邻
                    if(ry == x) { //3次链接
                        link(ly, x);
                        link(x, y);
                        link(y, rx);
                    }
                    else if(r[x] == y) { //3次链接
                        link(lx, y);
                        link(y, x);
                        link(x, ry);
                    }
                    //不相邻
                    else { //4次链接
                        link(lx, y);
                        link(y, rx);
                        link(ly, x);
                        link(x, ry);
                    }
                }
            }
        }
        long long sum = 0, t = 0; //总和, 下标
        for(int i = 1; i <= n; ++i) {
            t = r[t];
            if(i % 2 == 1)
                sum += t; //奇数位的和
        }
        if(flag && n % 2 == 0) //长度n为偶数 且 需要翻转
            sum = (long long)n * (n + 1) / 2 - sum; //总和 - 奇数位的和

        cout<<"Case "<<number++<<": "<<sum<<endl;

    }

    return 0;
}

🥇总结

初步接触了单双链表,循环链表,静态链表

链表的优势在于,插入和删除

顺序表的话,只有一个优点,取元素比较快,也就是查找,O(1)复杂度

静态链表的优势除了插入,删除,还有查找(下标)--> 综合了链表和顺序表的优点

链表由结构体中的指针域和数据域构成

静态链表由多个数组代替数据域和指针域

什么叫入门?学完知识,刷上10题叫入门,现在才3题,入门还算不上...慢慢来

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

千帐灯无此声

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值