基础数据结构——链表

链表

前置芝士:一维数组

数组是最基础的数据结构,支持 O ( 1 ) \Omicron (1) O(1)随机访问(能够查询访问任意下标的值),但欲插入(或删除)某一下标的数,其后的所有数字需移动以腾出空位(或填补空位),其插入或删除操作时间复杂度为 O ( n ) \Omicron(n) O(n).可见数组的插入和删除效率是极其低下的.

为方便插入和删除操作,我们引入一种线性链式数据结构——链表.

链表支持 O ( 1 ) \Omicron(1) O(1)插入和删除,却不支持随机访问,只能按照顺序访问其中元素。链表通常采用指针以及动态分配内存方式实现,但是我不会。因此我们采用数组模拟链表.

单向链表

首先定义一个 i n t int int类型变量 h e a d head head表示链表头, t a i l tail tail表示表尾,数组 n u m [ i ] num[i] num[i] n x t [ i ] nxt[i] nxt[i]分别表示第 i i i个节点所存储的值以及其链接的下一个节点编号, c n t cnt cnt是一个简单计数器,用于为新节点赋编号.

操作如下:

const ll N=1e5+5;
ll head, cnt, tail;
ll num[N], nxt[N];

void build() {
    tail=head=++cnt; //新建一个链表,表头为cnt
}

void Insert(int pos, int y) {
    num[++cnt]=y;         //新开一个节点cnt,存储数值y
    nxt[cnt]=nxt[pos];  //将原先pos的后继作为新节点cnt的后继
    nxt[pos]=cnt;         //原先pos的后继变为新节点cnt
    if (pos==tail) tail=cnt; //若在尾部插入,则更新表尾
    //通过Insert操作,我们可以在链表的节点pos后插入带有数值为y的新节点cnt
    //cnt链接的下个节点是插入前pos的后继,而pos新的后继为cnt
    //这样就达到了在pos与nxt[pos]之间插入cnt的目的
}

void access() {  //链表的遍历
    for (int i=nxt[head]; i; i=nxt[i]) { 
//若链表头不存储数值,则nxt[head]为第一个存储数值的节点,最后一个节点遍历完,其nxt未赋值,为0,故遍历到0结束
        cout<<num[i];
    }
}

void del(int goal) { //删除节点goal,首先需要找到pos的前驱节点
    for (int i=head; i; i=nxt[i]) {
        if (nxt[i]==goal){        //如果节点i的后继是goal,那i即为goal的前驱
            nxt[i]=nxt[goal];   //将goal的后继继承给i,后续遍历便访问不到goal,即goal被删除
            if (goal==tail) tail=i; //更新表尾
            break;                //删除完毕,终止遍历
        }
    }
}

从代码可见,若已知目标节点,单项链表的插入操作是 O ( 1 ) \Omicron(1) O(1)的,但是其删除操作却是 O ( n ) \Omicron(n) O(n),原因在于我们需要找到待删除节点的前驱节点.

如果我们记录每个节点的前驱,这样查询前驱的复杂度就降低为 O ( 1 ) \Omicron(1) O(1).同时记录节点前驱和后继的链表,就是双向链表.

双向链表

顾名思义,支持双向访问的链表.在单向链表的基础上,对每个节点添加一个 p r e pre pre变量. p r e [ i ] pre[i] pre[i]表示第 i i i个节点的前驱.特别地, p r e [ h e a d ] = 0 pre[head]=0 pre[head]=0

插入操作思路与单向链表基本一致,注意前驱转换的细节即可.

void Insert(int pos, int y) {//在pos后插入新节点,存储数值y
    num[++cnt]=y;         //新开一个节点cnt,存储数值y
    nxt[cnt]=nxt[pos];  //将原先pos的后继作为新节点cnt的后继
    nxt[pos]=cnt;         //原先pos的后继变为新节点cnt
    pre[nxt[cnt]]=cnt;  //cnt后继的前驱是cnt本身,故记录为cnt
    pre[cnt]=pos;       //cnt插入在pos之后,故pre[cnt]=pos
}

区别于单向链表的是,双向链表支持 O ( 1 ) \Omicron(1) O(1)查询前驱,所以其删除操作是 O ( 1 ) \Omicron(1) O(1)的.

void del(int goal) {   //删除节点goal,首先需要找到pos的前驱节点
    ll rem=pre[goal];  //节点goal的前驱记录为rem
    nxt[rem]=nxt[goal];//rem的后继继承goal的后继
    pre[nxt[goal]]=rem;//goal后继的前驱继承goal的前驱
    //此操作过后再次访问链表,访问到rem时下一个节点即nxt[goal]
    //由此,goal节点已从链表中删去
}
循环链表

分为单项循环链表和双向循环链表.

其实两种链表的实现方式是相一致的,我们只需在建立空链表(仅有表头)的时候,改为

    head=++cnt; nxt[head]=head;
    pre[head]=head; //若为双向

这样若后续插入节点,我们尾节点的后继会继承为 h e a d head head,如此便可循环遍历链表.当然,在循环链表中我们一般会为 h e a d head head节点赋值,这样 h e a d head head节点的地位与其余节点是等价的,也符合循环的概念(没头没尾).

例题:P1996 约瑟夫问题

题目描述:

n n n 个人围成一圈,从第一个人开始报数,数到 m m m 的人出列,再由下一个人重新从 1 1 1 开始报数,数到 m m m 的人再出圈,依次类推,直到所有的人都出圈,请输出依次出圈人的编号。

思路:

构建双向循环链表,朝一个方向边遍历边计数,计数至 m m m时将当前遍历到的节点删去,然后重新计数,直到仅剩下一人.

code:
const int N=10000;
int pre[N], nxt[N], n, m, cnt;
//cnt 表示出局人数
int main() {
    scanf("%d %d", &n, &m);
    for (int i=1; i<=n; i++) nxt[i]=i+1, pre[i]=i-1;  //链接建表
    nxt[n]=1, pre[1]=n;                              //形成循环
    int now=1, count=0;  //当前遍历位置now, 当前计数count
    while(cnt<n-1) {    //n-1个人出局时结束循环
        ++count;
        if (count==m) {
            count=0; //计数器清零
            pre[nxt[now]]=pre[now];
            nxt[pre[now]]=nxt[now];
            ++cnt;
            //删除now,当前人出局
            printf("%d ", now);
        }
        now=nxt[now]; //继续往下遍历
    }
    printf("%d\n", now);
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值