链表理论基础

链表

什么是链表,链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。

链表的入口节点称为链表的头结点也就是head。

链表的类型

单链表

双链表

  • 单链表中的指针域只能指向节点的下一个节点。
  • 双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
  • 双链表 既可以向前查询也可以向后查询。

循环链表

  • 循环链表,顾名思义,就是链表首尾相连。
  • 循环链表可以用来解决约瑟夫环问题。

链表的存储方式

  • 了解完链表的类型,再来说一说链表在内存中的存储方式。
  • 数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。
  • 链表是通过指针域的指针链接在内存中各个节点。
  • 所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。

    这个链表起始节点为2, 终止节点为7, 各个节点分布在内存的不同地址空间上,通过指针串联在一起

链表节点的定义

java 复制代码
// 单链表
struct ListNode {
    int val;  // 节点上存储的元素
    ListNode *next;  // 指向下一个节点的指针
    ListNode(int x) : val(x), next(NULL) {}  // 节点的构造函数
};

删除节点

删除D节点,如图所示:

bash 复制代码
只要将C节点的next指针 指向E节点就可以了。
那有同学说了,D节点不是依然存留在内存里么?只不过是没有在这个链表里而已。
是这样的,所以在C++里最好是再手动释放这个D节点,释放这块内存。
其他语言例如Java、Python,就有自己的内存回收机制,就不用自己手动释放了。

添加节点

如图所示:

bash 复制代码
可以看出链表的增添和删除都是O(1)操作,也不会影响到其他节点。
但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。

性能分析

再把链表的特性和数组的特性进行一个对比,如图所示:

关于链表的题目很多,这里不再给出,下面来看一下关于链表的基本操作:

下面这道题目基本包含了链表的重要的几个操作

707.设计链表

题目链接

题意:

在链表类中实现这些功能:

get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。

addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。

addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。

addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。

deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。

java 复制代码
//实现单链表
class MyLinkedList {
public:

     struct ListNode{
        int val;
        ListNode* next;
        ListNode(int x):val(x),next(nullptr){}
     };
    

    MyLinkedList() {
        dummyHead = new ListNode(0);
        _size = 0;
    }
    
    int get(int index) {
        if(index < 0||index>= _size) return -1;
        ListNode* cur = dummyHead->next;
        while(index--){
            cur = cur->next;
        }
        return cur->val;
    }
    
    void addAtHead(int val) {
        ListNode* newnode = new ListNode(val);
        newnode->next = dummyHead->next;
        dummyHead->next = newnode;
        _size++;
    }
    
    void addAtTail(int val) {
        ListNode* newnode = new ListNode(val);
        ListNode* cur = dummyHead;
        while(cur->next != nullptr){
            cur = cur->next;
        }
        cur->next = newnode;
        _size++;
        
    }
    
    void addAtIndex(int index, int val) {
        if(index < 0||index>_size) return ;
        if(index == 0) {addAtHead(val); return ;} 
        if(index == _size) {addAtTail(val);return; }

        ListNode* newnode = new ListNode(val);
        ListNode* cur = dummyHead;
        while(index--){
            cur = cur->next;
        }
         newnode ->next = cur->next;
         cur->next  = newnode;
         _size++;
    }
    
    void deleteAtIndex(int index) {
        if(index < 0||index>=_size) return ;
         ListNode* cur = dummyHead;
        while(index--){
            cur = cur->next;
        }
        ListNode* tmp = cur->next;
        cur->next = tmp->next;
        delete tmp;
        _size--;
    }

private:
       ListNode* dummyHead;
       int _size;
};

/**
 * Your MyLinkedList object will be instantiated and called as such:
 * MyLinkedList* obj = new MyLinkedList();
 * int param_1 = obj->get(index);
 * obj->addAtHead(val);
 * obj->addAtTail(val);
 * obj->addAtIndex(index,val);
 * obj->deleteAtIndex(index);
 */
java 复制代码
//采用循环虚拟结点的双链表实现
class MyLinkedList {
public:
    // 定义双向链表节点结构体
    struct DList {
        int elem; // 节点存储的元素
        DList *next; // 指向下一个节点的指针
        DList *prev; // 指向上一个节点的指针
        // 构造函数,创建一个值为elem的新节点
        DList(int elem) : elem(elem), next(nullptr), prev(nullptr) {};
    };

    // 构造函数,初始化链表
    MyLinkedList() {
        sentinelNode = new DList(0); // 创建哨兵节点,不存储有效数据
        sentinelNode->next = sentinelNode; // 哨兵节点的下一个节点指向自身,形成循环
        sentinelNode->prev = sentinelNode; // 哨兵节点的上一个节点指向自身,形成循环
        size = 0; // 初始化链表大小为0
    }

    // 获取链表中第index个节点的值
    int get(int index) {
        if (index > (size - 1) || index < 0) { // 检查索引是否超出范围
            return -1; // 如果超出范围,返回-1
        }
        int num;
        int mid = size >> 1; // 计算链表中部位置
        DList *curNode = sentinelNode; // 从哨兵节点开始
        if (index < mid) { // 如果索引小于中部位置,从前往后遍历
            for (int i = 0; i < index + 1; i++) {
                curNode = curNode->next; // 移动到目标节点
            }
        } else { // 如果索引大于等于中部位置,从后往前遍历
            for (int i = 0; i < size - index; i++) {
                curNode = curNode->prev; // 移动到目标节点
            }
        }
        num = curNode->elem; // 获取目标节点的值
        return num; // 返回节点的值
    }

    // 在链表头部添加节点
    void addAtHead(int val) {
        DList *newNode = new DList(val); // 创建新节点
        DList *next = sentinelNode->next; // 获取当前头节点的下一个节点
        newNode->prev = sentinelNode; // 新节点的上一个节点指向哨兵节点
        newNode->next = next; // 新节点的下一个节点指向原来的头节点
        size++; // 链表大小加1
        sentinelNode->next = newNode; // 哨兵节点的下一个节点指向新节点
        next->prev = newNode; // 原来的头节点的上一个节点指向新节点
    }

    // 在链表尾部添加节点
    void addAtTail(int val) {
        DList *newNode = new DList(val); // 创建新节点
        DList *prev = sentinelNode->prev; // 获取当前尾节点的上一个节点
        newNode->next = sentinelNode; // 新节点的下一个节点指向哨兵节点
        newNode->prev = prev; // 新节点的上一个节点指向原来的尾节点
        size++; // 链表大小加1
        sentinelNode->prev = newNode; // 哨兵节点的上一个节点指向新节点
        prev->next = newNode; // 原来的尾节点的下一个节点指向新节点
    }

    // 在链表中的第index个节点之前添加值为val的节点
    void addAtIndex(int index, int val) {
        if (index > size) { // 检查索引是否超出范围
            return; // 如果超出范围,直接返回
        }
        if (index <= 0) { // 如果索引为0或负数,在头部添加节点
            addAtHead(val);
            return;
        }
        int num;
        int mid = size >> 1; // 计算链表中部位置
        DList *curNode = sentinelNode; // 从哨兵节点开始
        if (index < mid) { // 如果索引小于中部位置,从前往后遍历
            for (int i = 0; i < index; i++) {
                curNode = curNode->next; // 移动到目标位置的前一个节点
            }
            DList *temp = curNode->next; // 获取目标位置的节点
            DList *newNode = new DList(val); // 创建新节点
            curNode->next = newNode; // 在目标位置前添加新节点
            temp->prev = newNode; // 目标位置的节点的前一个节点指向新节点
            newNode->next = temp; // 新节点的下一个节点指向目标位置的结点
            newNode->prev = curNode; // 新节点的上一个节点指向当前节点
        } else { // 如果索引大于等于中部位置,从后往前遍历
            for (int i = 0; i < size - index; i++) {
                curNode = curNode->prev; // 移动到目标位置的后一个节点
            }
            DList *temp = curNode->prev; // 获取目标位置的节点
            DList *newNode = new DList(val); // 创建新节点
            curNode->prev = newNode; // 在目标位置后添加新节点
            temp->next = newNode; // 目标位置的节点的下一个节点指向新节点
            newNode->prev = temp; // 新节点的上一个节点指向目标位置的节点
            newNode->next = curNode; // 新节点的下一个节点指向当前节点
        }
        size++; // 链表大小加1
    }

    // 删除链表中的第index个节点
    void deleteAtIndex(int index) {
        if (index > (size - 1) || index < 0) { // 检查索引是否超出范围
            return; // 如果超出范围,直接返回
        }
        int num;
        int mid = size >> 1; // 计算链表中部位置
        DList *curNode = sentinelNode; // 从哨兵节点开始
        if (index < mid) { // 如果索引小于中部位置,从前往后遍历
            for (int i = 0; i < index; i++) {
                curNode = curNode->next; // 移动到目标位置的前一个节点
            }
            DList *next = curNode->next->next; // 获取目标位置的下一个节点
            curNode->next = next; // 删除目标位置的节点
            next->prev = curNode; // 目标位置的下一个节点的前一个节点指向当前节点
        } else { // 如果索引大于等于中部位置,从后往前遍历
            for (int i = 0; i < size - index - 1; i++) {
                curNode = curNode->prev; // 移动到目标位置的后一个节点
            }
            DList *prev = curNode->prev->prev; // 获取目标位置的下一个节点
            curNode->prev = prev; // 删除目标位置的节点
            prev->next = curNode; // 目标位置的下一个节点的下一个节点指向当前节点
        }
        size--; // 链表大小减1
    }

private:
    int size; // 链表的大小
    DList *sentinelNode; // 哨兵节点的指针
};
相关推荐
workflower3 小时前
单元测试-例子
java·开发语言·算法·django·个人开发·结对编程
LXS_3574 小时前
Day 05 C++ 入门 之 指针
开发语言·c++·笔记·学习方法·改行学it
MicroTech20255 小时前
微算法科技(MLGO)研发突破性低复杂度CFG算法,成功缓解边缘分裂学习中的掉队者问题
科技·学习·算法
墨染点香5 小时前
LeetCode 刷题【126. 单词接龙 II】
算法·leetcode·职场和发展
aloha_7896 小时前
力扣hot100做题整理91-100
数据结构·算法·leetcode
Tiny番茄6 小时前
31.下一个排列
数据结构·python·算法·leetcode
挂科是不可能出现的6 小时前
最长连续序列
数据结构·c++·算法
_Aaron___6 小时前
List.subList() 返回值为什么不能强转成 ArrayList
数据结构·windows·list
前端小L7 小时前
动态规划的“数学之魂”:从DP推演到质因数分解——巧解「只有两个键的键盘」
算法·动态规划
RTC老炮7 小时前
webrtc弱网-ReceiveSideCongestionController类源码分析及算法原理
网络·算法·webrtc