单链表-双链表

单链表

题目

实现一个单链表,链表初始为空,支持三种操作:

  1. 向链表头插入一个数;
  2. 删除第 k 个插入的数后面的一个数;
  3. 在第 k 个插入的数后插入一个数。

现在要对该链表进行 M 次操作,进行完所有操作后,从头到尾输出整个链表。

注意:
题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。

输入格式
第一行包含整数 M ,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:

  1. H x,表示向链表头插入一个数 x 。
  2. D k,表示删除第 k 个插入的数后面的数(当 k 为 0 时,表示删除头结点)。
  3. 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

第 k 个插入的元素在哪里?

在本道题目中,有两个问题:

删除第 k 个插入的数后面的数
在第 k 个插入的数后插入一个数

先解释一下什么叫第k个插入的元素

  1. 把插入操作按先后排序,第 k 次执行插入操作的那个元素。
  2. 并不是链表中从前往后数第 k 个元素。

在链表中删除指针指向的元素的后一个元素,或者在指针指向的某个元素后面插入一个新元素是很容易实现的。

所以,只要弄明白第 k 个插入的数的指针在哪里,这两个问题就很容易解决。

来分析一下插入操作:

  1. 链表为空的时候:idx 的值为 0,
  2. 插入第一个元素 a 后:e[0] = a, idx 的值变为 1,
  3. 插入第二个元素 b 后:e[1] = b, idx 的值变为 2,
  4. 插入第三个元素 c 后:e[2] = c, idx 的值变为 3,
    所以: 第 k 个出入元素的索引值 k - 1

有人会说,如果中间删除了某些元素呢?

在看一下伴随着删除操作的插入:

  1. 链表为空的时候:idx 的值为 0,
  2. 插入第一个元素 a 后:e[0] = a, idx 的值变为 1,
  3. 插入第二个元素 b 后:e[1] = b, idx 的值变为 2,
  4. 删除第一个插入的元素 a:head 变为 1, idx 不变,依旧为 2。
  5. 删除第二个插入的元素 b:head 变为 2, idx 不变,依旧为 2。
  6. 插入第三个元素 c 后:e[2] = c, idx 的值变为 3。

所以删除操作并不改变第 k 个插入元素的索引。

故第 k 个元素的索引一定是 k - 1

题解:

head 表示头结点,e数组存储元素,ne数组存储下一个节点索引,indx表示下一个可以存储元素的位置索引。

头结点后面添加元素:

  1. 在e的idx处存储元素e[ide] = x;

  2. 该元素插入到头结点后面 ne[idx] = head;

  3. 头结点指向该元素 head = idx;

  4. idx 指向下一个可存储元素的位置 idx++。

在索引 k 后插入一个数

  1. 在e的idx处存储元素e[index] = x

  2. 该元素插入到第k个插入的数后面 ne[idx] = ne[k];

  3. 第k个插入的数指向该元素 ne[k] = idx;

  4. idx 指向下一个可存储元素的位置 idx++。

删索引为 k 的元素的后一个元素:

  1. ne[k] 的值更新为 ne[ne[k]]
    画个图
    以题目为例:
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

最终结果如下:

在这里插入图片描述

代码

#include <iostream>
using namespace std;
const int N = 100010;

int head, e[N], ne[N], idx;

void init()
{
    head = -1;
    idx = 0;
}

void add_to_head(int x)
{
    e[idx] = x, ne[idx] = head, head = idx++;
}

void add(int k, int x)
{
    e[idx] = x, ne[idx] = ne[k], ne[k] = idx++;
}

void remove(int k)
{
    ne[k] = ne[ne[k]];
}

int main()
{
    int m;
    cin >> m;

    init();

    while(m--)
    {
        char op;
        int k, x;
        cin >> op;
        if(op == 'H')
        {
            cin >> x;
            add_to_head(x);
        }
        else if(op == 'D')
        {
            cin >> k;
            if(!k) head = ne[head];
            else remove(k - 1);//第k个元素对应的索引为k - 1
        }
        else 
        {
            cin >> k >> x;
            add(k - 1, x);//第k个元素对应的索引为k - 1
        }
    }
    for(int i = head; i != -1; i = ne[i]) cout << e[i] << " ";

    cout<< endl;
}

Java

import java.util.Scanner;
public class Main{
        static int[] e = new int[100010];
        static int[] ne = new int[100010];
        static int idx,head;
        public static void init(){
            idx = 0;
            head = -1;
        }
    //H向链表头插入一个数x;
    public static void add_head(int x){
        e[idx] = x;
        ne[idx] = head;
        head = idx++;
    } 
    //I在第k位数后面插入一个数x
    public static void add(int k,int x){
        e[idx] = x;
        ne[idx] = ne[k];
        ne[k] = idx++;
    }
    //D删除第k个数后面得数
    public static void remove(int k){
        ne[k] = ne[ne[k]];
    }
    public static void main(String[] args){
         Scanner scan = new Scanner(System.in);
        int m = scan.nextInt();

        init();
        while(m -- > 0){
            //因为java中没有输入一个字符,所以用字符串转字符
            String s = scan.next();
            char op = s.charAt(0);

            if(op == 'H'){
                int x = scan.nextInt();
                add_head(x);
            }else if(op == 'D'){
                int k = scan.nextInt();
                if(k == 0) head = ne[head];
                else remove(k-1);
            }else {
                int k = scan.nextInt();
                int x = scan.nextInt();
                add(k-1,x);
            }
        }
        for(int i = head;i != -1;i = ne[i] ){
            System.out.print(e[i] +  " ");
        }

    }

}

为什么不用课本上学的结构体来构造链表??

学过数据结构课的人,对链表的第一反应就是:

链表由节点构成,每个节点保存了 下一个元素的位置 这两个信息。节点的表示形式如下:

class Node{
public:
    int val;
    Node* next;
};

这样构造出链表节点的是一个好方法,也是许多人一开始就学到的。

使用这种方法,在创建一个值为 x 新节点的时候,语法是:

Node* node = new Node();
node->val = x

看一下创建新节点的代码的第一行:

Node* node = new Node();,中间有一个 new 关键字来为新对象分配空间。

new的底层涉及内存分配,调用构造函数,指针转换等多种复杂费时的操作。一秒大概能new1w次左右。

在平时的工程代码中,不会涉及上万次的new操作,所以这种结构是一种 见代码知意 的好结构。

但是在算法比赛中,经常碰到操作在10w级别的链表操作,如果使用结构体这种操作,是无法在算法规定时间完成的。

所以,在算法比赛这种有严格的时间要求的环境中,不能频繁使用new操作。也就不能使用结构体来实现数组。

双链表

双链表原理同单链表,只是多实现了左右两端添加节点

实现一个双链表,双链表初始为空,支持 5 种操作:

  1. 在最左侧插入一个数;
  2. 在最右侧插入一个数;
  3. 将第 k 个插入的数删除;
  4. 在第 k 个插入的数左侧插入一个数;
  5. 在第 k 个插入的数右侧插入一个数

现在要对该链表进行 M 次操作,进行完所有操作后,从左到右输出整个链表。

注意:
题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。

输入格式
第一行包含整数 M ,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:

  1. L x,表示在链表的最左端插入数 x 。
  2. R x,表示在链表的最右端插入数 x 。
  3. D k,表示将第 k 个插入的数删除。
  4. IL k x,表示在第 k 个插入的数左侧插入一个数。
  5. IR k x,表示在第 k 个插入的数右侧插入一个数。

输出格式
共一行,将整个链表从左到右输出。

数据范围
1 ≤ M ≤ 100000

所有操作保证合法。

输入样例
10
R 7
D 1
L 3
IL 2 10
D 3
IL 2 7
L 8
R 9
IL 4 7
IR 2 2
输出样例:
8 7 7 3 2 9

代码详解:

#include <iostream>
using namespace std;
const int N = 100010;
int e[N], l[N], r[N],index = 0;

void init()
{
    //如下初始化导致第 a 个插入的元素的索引为: a + 1
    l[1] = 0;
    r[0] = 1;
    index = 2;
} 

// 在索引为 a 的右侧插入x,对应第 a - 1 个插入元素右侧插入 x
void insert(int a, int x)
{
    e[index] = x;
    l[index] = a;
    r[index] = r[a];
    l[r[a]] = index;
    r[a] = index++;
}

//删除索引为 a 的元素,对应于删除第 a - 1 个插入元素。
void remove(int a)
{
    r[l[a]] = r[a];
    l[r[a]] = l[a];
}

int main()
{
    init();
    int m;
    cin>>m;
    while(m--)
    {
        string op;
        cin >> op;
        int k, x;
        if(op == "L")
        {
            cin >> x;
            insert(0, x);
        }
        else if(op == "R")
        {
            cin >> x;
            insert(l[1], x);
        }
        else if (op == "D")
        {
            cin >> k;
            remove(k + 1);//第 k 个插入元素对应的索引为 k + 1
        }
        else if (op == "IL")
        {
            cin >> k >> x;
            insert(l[k + 1], x);// 第 k 个插入元素对应的索引为 k + 1, l[k + 1] 为链表中上一个位置对应的索引
        }
        else
        {
            cin >> k >> x;
            insert(k + 1, x);//第 k 个插入元素对应的索引为 k + 1
        }
    }
    for (int i = r[0]; i != 1; i = r[i]) cout << e[i] << ' ';
    cout << endl;

    return 0;

}

Java

import java.util.Scanner;
public class Main{
    static int N = 100010;
    static int[] e = new int[N];
    static int[] r = new int[N];
    static int[] l = new int[N];
    static int idx;

    //删除第k位插入的数
    public static void remove(int k){
        r[l[k]] = r[k];
        l[r[k]] = l[k];
    }
    //这是在第k位数后面插入一个数x
    public static void add_all(int k,int x){
        e[idx] = x;
        r[idx] = r[k];
        l[idx] = k;
        l[r[k]] = idx;
        r[k] = idx++;
    }
    public static void main(String[] args){
        Scanner scan = new Scanner(System.in);
        int m = scan.nextInt();

        r[0] = 1;l[1] = 0;idx = 2;

        while(m -- > 0){
            String s = scan.next();
            char op = s.charAt(0);
            if(op == 'L'){
                int x = scan.nextInt();
                add_all(0,x);
            }else if(op == 'R'){
                int x = scan.nextInt();
                add_all(l[1],x);
            }else if(op == 'D'){
                int k = scan.nextInt();
                remove(k + 1);
            }else if(s.equals("IR")){
                int k  = scan.nextInt();
                int x = scan.nextInt();
                add_all(k + 1,x);
            }else {
                int k = scan.nextInt();
                int x = scan.nextInt();
                add_all(l[k+1],x);
            }
        }
       for(int i = r[0];i != 1; i= r[i]){
            System.out.print(e[i]  + " ");
        }
    }
}

  • 24
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值