链表理论基础

链表

什么是链表,链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向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; // 哨兵节点的指针
};
相关推荐
2401_845417452 小时前
哈希表原理详解
数据结构·哈希算法·散列表
charlie1145141912 小时前
精读 C++20 设计模式:行为型设计模式 — 状态机模式
c++·学习·设计模式·状态模式·c++20
蒋星熠2 小时前
脑机接口(BCI):从信号到交互的工程实践
人工智能·python·神经网络·算法·机器学习·ai·交互
liuyao_xianhui3 小时前
四数之和_优选算法(C++)双指针法总结
java·开发语言·c++·算法·leetcode·职场和发展
合作小小程序员小小店3 小时前
桌面预测类开发,桌面%雷达,信号预测%系统开发,基于python,tk,scikit-learn机器学习算法实现,桌面预支持向量机分类算法,CSV无数据库
python·算法·机器学习·支持向量机·scikit-learn
CAir23 小时前
CGO 原理
c++·go·cgo
java1234_小锋3 小时前
Scikit-learn Python机器学习 - 聚类分析算法 - Agglomerative Clustering(凝聚层次聚类)
python·算法·机器学习
墨染点香3 小时前
LeetCode 刷题【93. 复原 IP 地址】
算法·leetcode·职场和发展
磨十三3 小时前
C++ 中的类型双关、union 与类型双关:让一块内存有多个“名字”
开发语言·c++