首页 > 其他分享 >数据结构-链表 day 2

数据结构-链表 day 2

时间:2025-01-14 16:33:57浏览次数:3  
标签:head idx int ne 链表 插入 数据结构 day

数据结构-链表

单链表

一般在算法里面都是采用的静态链表,动态链表

单链表一般就是邻接表,包括存储树与图

双链表一般是优化某些问题的

一下是动态链表与静态链表之间的区别

. 内存分配方式

静态链表

• 静态链表通常是基于一个固定大小的数组来实现的。链表中的每个结点在数组中占据一个位置,通过数组下标来模拟指针的功能。

• 每个结点除了存储数据外,还需要一个指向下一个结点的位置(数组的下标或索引)。

• 静态链表的内存大小在编译时确定,不能动态扩展。

动态链表

• 动态链表使用的是 指针 来动态分配内存。每次需要插入一个新结点时,程序会调用 malloc() 或 new 操作符(取决于编程语言)来动态分配内存空间

• 动态链表的内存是按需分配的,因此可以随时扩展或收缩,内存使用效率较高。

2. 内存管理

静态链表

• 由于静态链表基于数组,内存是一次性分配的。数组的大小在编译时固定,因此如果链表的实际大小小于数组的大小,会浪费内存;如果链表的大小超过了预设的数组大小,则无法继续扩展。

• 静态链表没有真正的指针,而是通过数组下标来模拟指针关系。

动态链表

• 动态链表的内存管理由操作系统或运行时环境来负责,程序可以在运行时动态申请和释放内存。

• 每个结点可以独立分配内存,因此内存空间的使用更灵活,能够根据链表实际的大小来管理内存。

3. 空间效率

静态链表

• 由于预先分配了固定大小的内存,因此空间利用率可能较低。特别是当链表的元素数目变化很大时,静态链表可能会浪费很多内存。

动态链表

• 空间利用率较高。每个结点的内存空间是动态分配的,只有当需要更多空间时才会分配额外的内存。

4. 灵活性

静态链表

• 静态链表的灵活性较差,因为它的大小是固定的,一旦数组大小确定,无法改变。

动态链表

• 动态链表的灵活性较强,可以根据实际需要动态增减结点,适应不同的链表大小。

5. 实现方式

静态链表

• 通常通过一个结构体和一个固定大小的数组来实现。数组中的每个元素都包含数据部分和指向下一个结点的“下标”或“索引”,模拟指针的功能。

动态链表

• 每个结点包含两个部分:数据部分和一个指针(或者引用)指向下一个结点。内存是动态分配的,因此每个结点都是独立的。

静态链表不同与动态链表,他一般是不用->next 指针的,所以

可以参考这个图片,下面是一个单链表的程序

void init()
{
    head = -1;
    idx = 0;
}
​
// 将x插到头结点
void add_to_head(int x)
{
    e[idx] = x, ne[idx] = head, head = idx ++ ;
}
​
// 将x插到下标是k的点后面
void add(int k, int x)
{
    e[idx] = x, ne[idx] = ne[k], ne[k] = idx ++ ;
}
​
// 将下标是k的点后面的点删掉
void remove(int k)
{
    ne[k] = ne[ne[k]];
}
​
​

这个就是大体的头插,删除,插在某一个节点的后面

现在来画图解释一下头插

head = idx;   // 将当前的 `idx` 赋值给 `head`,使头节点指向当前结点。
idx = idx + 1; // 自增 `idx`,为下一次插入操作预留新的数组位置。

这里是一个拆分,也许这样你就理解了为什么是idx++了

现在再来解释一下再一个节点(不是头节点插入)

下面在画一个删除节点的

这个就简单一点吧

现在在回到刚刚那个题,现在我们来上代码

#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--)
    {
        int k,x;
        char op;
        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);
        }
        else {
            cin >> k >> x;
            add(k-1,x);
        }
    }
    for(int i = head;i != -1;i = ne[i])  cout << e[i] << ' ';
    
    cout << endl;
    return 0;
}

这里面最重要的是add(k-1,x) 这里,因为很容易搞错

双链表

因为只写代码的话很多同学就不明白为什么是这样的,所以现在我画个图

​​​​​​​

大体上就是这样的,根据这个图,我们就要进行4次修改才可以

void add(int k,int x)
{
  e[idx] = x;  //对当前我要插入的点赋值
  r[idx] = r[k] ; //这里指的是将要添加的值的右边箭头指向k位置的右边箭头指向的值
  l[idx] = k;
  l[r[k]] = idx;
  r[k] = idx;
}  四个指针操作,主要是画图就理解深刻; 
​

那么这里是插入,如果是删除嫩?我应该怎么写呢

void remove(int k) //如果我要删除第k的点
{
  r[l[k]] = r[k];
  l[r[k]] = l[k];
}

我一直都是写的数组,推荐写数组,因为写数组比写结构体简单

1. 数组是连续存储,结构简单

• 数组是 连续内存块,通过索引直接访问元素,无需处理复杂的指针操作。

• 在实现链表时,数组模拟的静态链表通过下标来模拟指针(例如 next 数组存储后继位置),逻辑更直观。

• 而结构体需要真正的指针去指向动态分配的内存,涉及内存分配 (malloc 或 new)、释放 (free 或 delete) 和指针操作,这增加了复杂度。

下面为两个示例对比:

数组操作(静态链表)

e[idx] = x;        // 存储数据
next[idx] = head;  // 模拟指针,存储下标
head = idx++;

结构体操作(动态链表)

Node* newNode = new Node(); // 动态分配内存
newNode->data = x;          // 存储数据
newNode->next = head;       // 指向当前头节点
head = newNode;             // 更新头节点

为什么 idx = 2:

• 左端点(0)和右端点(1)被预留用于标记链表边界。

• 数据存储从下标 2 开始,逻辑清晰且避免冲突。

是否可以使用其他值

• 从 2 开始是最合理的选择,因为更小的值(0 或 1)会导致下标冲突,更大的值(3 或以上)会浪费空间。

左端点和右端点的作用

• 统一链表操作的边界处理,无需额外判断特殊情况。

所以一般设置idx从2开始

下面上代码

实现一个双链表,双链表初始为空,支持 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 m;
int e[N], l[N], r[N], idx;
​
// 在节点a的右边插入一个数x
void insert(int a, int x)
{
    e[idx] = x;
    l[idx] = a, r[idx] = r[a];
    l[r[a]] = idx, r[a] = idx ++ ;
}
​
// 删除节点a
void remove(int a)
{
    l[r[a]] = l[a];
    r[l[a]] = r[a];
}
​
int main()
{
    cin >> m;
​
    // 0是左端点,1是右端点
    r[0] = 1, l[1] = 0;
    idx = 2;
​
    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);
        }
        else if (op == "IL")
        {
            cin >> k >> x;
            insert(l[k + 1], x);
        }
        else
        {
            cin >> k >> x;
            insert(k + 1, x);
        }
    }
​
    for (int i = r[0]; i != 1; i = r[i]) cout << e[i] << ' ';
    cout << endl;
​
    return 0;
}
​

大体上,这个就是基于数组的单链表,双链表的实现。

标签:head,idx,int,ne,链表,插入,数据结构,day
From: https://blog.csdn.net/2401_87202881/article/details/145133605

相关文章

  • 数据结构—《二叉树的定义与特性》
    目录1.二叉树的定义2.特殊的二叉树1)满二叉树2)完全二叉树3)二叉排序树。4)平衡二又树。5)正则二文树3.二叉树的性质4.二叉树的存储结构1)顺序存储结构2)链式存储结构1.二叉树的定义二叉树是一种特殊的树形结构,其特点是每个结点至多只有两棵子树(即二叉树中不存在度大......
  • 数据结构------树
    前言:前面我们学习了栈和队列。今天我们来学习一种新的数据结构---------树。首先我们来了解一下树的概念。1.树的概念与结构前面我们学习过的顺序表,栈都是一种顺序结构。链表,队列是链式结构。今天学习的树也是一种链式结构。它是由n(n>=0)个有限节点组成一个具有层次关系......
  • 数据结构之链式二叉树
    前言:前面我们学习了树相关的概念及堆的实现,今天来看看二叉树的实现。正式实现二叉树前,我们先了解一些基础知识。对于二叉树基础知识不清楚的可以观看数据结构------树这篇文章,回顾一下知识,有助于理解二叉树。二叉树的遍历方式都有哪些呢?.前序遍历:按照根节点,左节点,右节......
  • 【轻松掌握数据结构与算法】哈希(Hashing)
    什么是哈希?哈希是一种将任意长度的数据转换为固定长度的数据的技术。这个固定长度的数据通常被称为哈希值或哈希码。哈希函数是实现这一转换的关键,它接受任意长度的输入,并产生一个固定长度的输出。为什么使用哈希?哈希的主要用途之一是快速查找数据。通过哈希函数,我们可以将......
  • 代码随想录Day35 | 01背包问题 二维,01背包问题 一维,416.分割等和子集
    代码随想录Day35|01背包问题二维,01背包问题一维,416.分割等和子集01背包问题二维动态规划经典问题dp定义:dp[i][j]表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少状态转移方程:dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+va......
  • Day13-【软考】长文!什么是散列表查找?以及所有的排序算法是怎样的?如何进行堆排序(重点!)?
    文章目录什么是散列表查找?计算出空间相同怎么办?排序有哪些概念?排序方法有哪些分类?什么是直接插入排序?(稳定)什么是希尔排序?什么是冒泡排序?(稳定)什么是快速排序?O(nlog2为底n为真数)什么是简单选择/直接选择排序?什么是堆排序(重点!)?O(nlog2为底n为真数)比简单的选择排序,有什么优势......
  • leetcode刷题记录(java)——参考代码随想录:数组 链表 哈希表
    四、题目之:代码随想录https://programmercarl.com/(1)代码随想录:数组704.二分查找classSolution{publicintsearch(int[]nums,inttarget){if(target<nums[0]||target>nums[nums.length-1]){return-1;}intleft=0......
  • 从零开始的python之旅(day2)
    从零开始的python之旅(day2)  今天主要学数据类型,类型处理方式和循环以及异常处理(当然还有数学。目前学到现在,我感觉python和c语言最大的区别就是,python更更更方便了,主要是前人栽树后人乘凉了,特别是对于元组类型和列表类型以及字符串处理中,python和c语言有相似,但是python更好读而......
  • Java-数据结构-栈与队列(常考面试题与单调栈)
    在上一篇的学习中,我们学习了栈和队列的基本知识,以及它们对应都有哪些方法,在什么应用场景下如何使用,并且还对它们进行了模拟实现,而其实对于栈和队列的相关知识还远不止于此,而今天我们就对栈与队列进行复盘,认识更多使用它们的场景,夯实代码功底吧~一、常考面试题-思维以下习题在......
  • Java算法 数据结构 栈 队列 优先队列 比较器
    目录栈Stack性质构造方法代码示例队列Queue性质构造方法代码示例优先队列PriorityQueue性质构造方法代码示例比较器1.Comparator接口的方法2.常见的内置比较器1.自然排序比较器(naturalOrder())2.逆序排序比较器(reverseOrder())3.nullsFirst()......