【链表】知识点总结与力扣题目整理

链表是什么

如果说数组的设计理念是利用数据实体在内存中连续存放的特性实现随机访问,那么链表的设计是为了能够利用内存中不连续的内存碎片,将地址指针包裹在原始数据中,通过指针访问下一个元素位置,或者上一个元素,一个元素只跟其相邻元素强关联,插入和删除数据时比数组方便。
根据一个数据节点包裹的指针数目可分为单向、双向链表
根据指针指向,可设计出局部成环、或首尾相连的循环链表。
特性:

  • 不支持随机访问,插入删除节点只影响相邻节点内存
  • 每个数据节点占用内存多了一个指针的空间

在这里插入图片描述

力扣题目整理

一、链表的增删改查

虚拟头节点的应用:
在链表结构中,对于头节点的操作和对于中间元素的操作往往有所不同,因为头节点前面没有节点,而我们遍历往往通过双指针一前一后迭代进行,这样在循环中可以访问到一个节点的前驱后继节点,为了可以一视同仁的使用双指针,可以在头节点前添加一个虚拟头节点,这个节点没有实际意义,只是为了方便我们遍历链表。

1.设计支持随机访问的链表

力扣链接
题目描述:
你可以选择使用单链表或者双链表,设计并实现自己的链表。

单链表中的节点应该具备两个属性:val 和 next 。val 是当前节点的值,next 是指向下一个节点的指针/引用。

如果是双向链表,则还需要属性 prev 以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。

实现 MyLinkedList 类:

MyLinkedList() 初始化 MyLinkedList 对象。
int get(int index) 获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1 。
void addAtHead(int val) 将一个值为 val 的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。
void addAtTail(int val) 将一个值为 val 的节点追加到链表中作为链表的最后一个元素。
void addAtIndex(int index, int val) 将一个值为 val 的节点插入到链表中下标为 index 的节点之前。如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。如果 index 比长度更大,该节点将 不会插入 到链表中。
void deleteAtIndex(int index) 如果下标有效,则删除链表中下标为 index 的节点。
题目分析:
设计需求:

  1. 在指定位置插入/删除
  2. 访问随机位置元素

设计方案:

  1. 采用了单向链表,如果为了更快访问末尾元素,可以使用双向链表,根据位置确定是从头还是尾开始遍历查找
  2. 存储了虚拟头、头、尾节点指针,以及链表长度
  3. 需要在某些情况下更新头节点、尾节点指针:
  • 在空链表中插入元素时、在头部/尾部插入元素时
  • 删除尾节点、头节点时
struct Node{
    int val;
    Node* next;
    Node():val(0),next(nullptr){};
    Node(int Val):val(Val),next(nullptr){};
    Node(int Val,Node* Next):val(Val),next(Next){};
};

class MyLinkedList {
public:
    Node* _vhead;
    Node* _head;
    Node* _tail;
    int _length;
    MyLinkedList() {
        _length = 0;
        _head = _tail = nullptr;
        _vhead = new Node();
        
    }
   
    int get(int index) {
        Node* temp = _head;
        int find = 0;
        while(temp != nullptr){
            if(find == index){
                return temp->val;
            }
            ++find;
            temp = temp->next;
        }
        return -1;
    }
    
    void addAtHead(int val) {
        Node* newNode = new Node(val,_head);
        _vhead->next = newNode;
        // 更新头指针
        _head = newNode;
        // 如果向空链表中添加元素,需要更新尾指针
        if(!_length)_tail = newNode;
        // 更新链表长度
        ++_length;
    }
    
    void addAtTail(int val) {
    	// 如果是空链表,则转化为向头部添加节点
        if(!_tail){
            addAtHead(val);
            return;
        }
		// 非空链表,利用尾指针节点
        Node* newNode = new Node(val,nullptr);
        _tail->next = newNode;
        _tail = newNode;
        ++_length;
    }
    
    void addAtIndex(int index, int val) {
	    // 如果向头部插入节点
        if(index == 0){
            addAtHead(val);
            return;
        }
        // 如果向尾部插入节点
        if(index == _length){
            addAtTail(val);
            return;
        }
        // 如果超出链表范围
        if(index > _length)return;
        
        // 向链表中间部位插入节点,需要用双指针一前一后遍历,不需要更新头尾节点指针
        int find = 0;
        Node* last = _vhead;
        Node* cur = _head;
        while(cur != nullptr){
            if(find == index){
                Node* newNode = new Node(val,cur);
                last->next = newNode;
                ++_length;
                return;
            }
            ++find;
            last = last->next;
            cur = cur->next;
        }   
    }
    
    void deleteAtIndex(int index) {
        // 空链表 或者 下标超出范围 时 直接返回
        // 空链表
        if(_length == 0)return;
        // 下标无效
        if(index < 0 || index >= _length)return;

        Node* last = _vhead;
        Node* cur = _head;
        int count = 0;
        while(cur != nullptr){
            if(count == index){
                last->next = cur->next;
                delete cur;
                // 根据情况更新 头尾节点指针
                // 如果删除尾节点
                if(index == _length - 1){
                    _tail = last;
                }
                // 如果删除头节点
                if(index == 0){
                    _head = last->next;
                }
                // 更新链表长度
                --_length;
                return;
            }
            ++count;
            last = last->next;
            cur = cur->next;
        }
    }
};

/**
 * 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);
 */

2.合并两个有序链表

力扣链接
题目描述:
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
题目分析:
通过递归合并,判断List1和List2的首节点Node1和Node2。
递推阶段:

  • 如果 Node1 <= Node2,则递推将Node1插入List2和去除了Node1的List1形成的子链头部
    在这里插入图片描述

  • 如果Node1 > Node2 ,则递推将Node2插入List1和去除了Node2的List2形成的子链头部
    在这里插入图片描述

回溯后处理阶段:更新头节点的next指向

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        if(list1 == nullptr)return list2;
        if(list2 == nullptr)return list1;
        if(list1->val <= list2->val){
            list1->next = mergeTwoLists(list1->next,list2);
            return list1;
        }
        else{
            list2->next = mergeTwoLists(list1,list2->next);
            return list2;
        }
    }
};

3.链表的排序

力扣链接
题目描述:
给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。
归并排序:
类似于数组排序,可以利用单元素拆分+有序子区间合并,也就是归并排序的思想来排序链表,这里面涉及到递归的思想,值得好好研究。

  • 单元素拆分:将链表通过递归的方法形成一个个函数栈空间,保存了单个结点,从单个节点和单个节点入手排序
  • 有序子区间合并:有序的两个子链表合并为有序链表的过程很简单,因此在递推结束后,从单个节点与单个节点排序开始,在回溯的过程中,不断合并两个有序的子链表。
  • 两个有序子链表的合并:如上题。
class Solution {
public:
    ListNode* sortList(ListNode* head) {
        return sortList(head,nullptr);
    }
    ListNode* merge(ListNode* list1,ListNode*list2){
        if(list1 == nullptr)return list2;
        if(list2 == nullptr)return list1;
        if(list1->val <= list2->val){
            list1->next = merge(list1->next,list2);
            return list1;
        }
        else{
            list2->next = merge(list1,list2->next);
            return list2;
        }
    }
    ListNode* sortList(ListNode* start, ListNode* end){
        // 1. 递推结束的条件:
        // 如果是空节点
        if(start == nullptr)return start;
        // 剩一个或两个节点
        // I.一个节点(start为节点,end = nullptr),切割
        // II.两个节点(start end 均为非空节点),切割前一个
        if(start->next == end){
            start->next = nullptr;
            return start;
        }
        // 2. 递推前的预处理
        // 子链表划分,平分为两个子链表
        ListNode* slow = start, *fast = start;
        while(fast != end){
            fast = fast->next;
            if(fast == end)break;
            slow = slow->next;
            fast = fast->next;
        }
        // 3. 递推 
        // 直到 所有区间只有一个节点,然后合并
        // 注意两个区间[start,slow) , [start,end]
        return merge(sortList(start,slow),sortList(slow,end));
    }

};

二、链表的基础操作:迭代

1.移除链表指定元素

力扣链接
题目描述:
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。

虚拟头节点迭代遍历:

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        ListNode* vhead = new ListNode(0,head);
        ListNode* cur = vhead;
        while(cur->next != nullptr){
            if(cur->next->val == val){
                ListNode* toDelete = cur->next;
                cur->next = cur->next->next;
                delete toDelete;
            }else{
                cur = cur->next;
            }
        }
        return vhead->next;
    }
};

递归:

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        if(!head)return nullptr;
        head->next = removeElements(head->next, val);
        return head->val == val ? head->next : head;
    }
};

2.反转链表指定区域

力扣链接
题目描述:
给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。

虚拟头节点迭代遍历:
思路很简单,维护五个指针即可。
对于长度为n的待反转区域,只需移动n-1次即可。
在这里插入图片描述

class Solution {
public:
    ListNode* reverseBetween(ListNode* head, int left, int right) {
        ListNode* vhead = new ListNode(0,head);
        ListNode* p0, *p1, *p2, *p2Left,*p2Right;
        p0 = p1 = p2 = p2Left = p2Right = vhead;
        // 初始化指针
        // p0 为待反转区域前一个
        // p1 为待反转区域第一个
        // p2 为待反转区域将要反转的下个节点
        // p2Left 为 p2 前一个节点
        for(int i = 1; i < left; ++i){
            p0 = p0->next;
        }
        p1 = p0->next;
        p2 = p1->next;
        p2Left = p1;
    
        // 从待反转区域的第二个元素开始,只需n-1次即可全部反转完毕
        for(int i = 1; i < right-left+1; ++i){
            p2Right = p2->next;
            p0->next = p2;
            p2->next = p1;
            p2Left->next = p2Right;
            p1 = p2;
            p2 = p2Right;
        }
        return vhead->next;
    }
};

三、链表的基础操作:递归

递归的过程是递推+回溯。

  • 递推:实际上是利用进入函数时创建的局部栈空间,来保存每一次进入函数时的局部变量,这个过程为递推阶段,将每个结点的指针以局部变量的形式保存在函数栈空间中。
  • 回溯:当函数执行完毕,会弹出此函数栈空间的局部变量,并根据保护的现场程序计数器(代码位置),返回执行函数体前的上一行代码,此时又回到了该函数的栈空间。
// 递归函数
void dfs(){
	// 1. 结束递推的条件: 一般就是递推到尾节点
	if(  ...  ) return;
	// 2. 进入递推的前处理
	......
	// 3. 开始进入递推
	dfs();
	// 4. 回溯过程的后处理
	...
	return;
}

从内存占用的角度想清楚这个过程,就知道该怎样写代码了。一般链表、树等容器将数据放在堆区,通过指针解析地址的方式操作这些数据,那么在递归的过程中,传参就有两种形式:

  • 指针值传递:这样传递的是一个指针变量,每个栈空间指针变量单独存在,回溯阶段操作的是不同的内存中的指针变量,他们直接指向堆区的某个数据,互相不会影响,不能在栈空间1中影响栈空间2中的指针指向。
  • 指向指针的指针传递:本质是所有的函数栈空间的不同指针变量最终都指向堆区同一个地址,这样在不同函数栈空间中,就可以以全局变量的形式操作同一个地址,尽管是以函数局部变量的形式操作的。

下图中函数参数left是指向指针变量的指针,而right是指向实际数据的指针,在递归的过程中,不同栈空间中的left都是指向同一个数据,任意一个栈空间中改变left指向,在其他栈空间中left解析出来的数据也跟着改变指向,达到了在函数中传递一个全局变量的效果。而right在每个函数栈空间中都是互相独立的,互不干扰,互不影响。
在这里插入图片描述

1.判断回文链表

力扣链接
题目描述:
给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
递归法:
类似于深度优先遍历,先从头节点递推到尾节点,并传递头节点,再从尾节点开始回溯,期间判断首位指针是否值相等。

  • 在递推阶段更新右指针
  • 在回溯阶段更新左指针
  • 使用指向指针的指针传递左节点指针
  • 使用指向数据的指针传递右节点指针
    在这里插入图片描述

 */
class Solution {
public:
// 利用双指针,左指针从链表头节点开始,
// 右指针从链表尾节点开始,两两比较 并在回溯递归阶段更新左指针
    bool isPalindrome(ListNode* head) {
        ListNode* temp = head;
        return dfs(&temp,temp);
    }
    
    // 思路:在递推阶段,传递链表的头节点,直到递推到尾部
    // 在回溯阶段,从后往前对比当前节点和 左指针节点

    // 本题核心难点:在递归时需要改变左指针指向的节点,如何在函数栈空间中实时更新临时变量?
    // 注意:指针值传递时,每个函数栈空间的指针变量互不影响,都指向函数入口时传递的地址
    // 为了在每个函数局部变量栈空间中传递相同的一个节点,需要使用指向指针的指针
    bool dfs(ListNode** left, ListNode* right){
        if(right == nullptr)return true;
        if(!dfs(left,right->next))return false;
        bool result = (*left)->val == right->val;
        *left = (*left)->next;
        return result;
        
    }
};

2.反转整个链表

力扣链接
题目描述:
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
递归法:从尾节点开始处理

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        // 1. 递推终止条件:
        // 递推到尾节点,返回尾节点
        if(head == nullptr || head->next == nullptr)return head;
        // 2. 递推前的预处理:保存尾节点
        // 我们需要保存新的头节点,也就是原来的尾节点
        // 3. 进入递推阶段,并拿到尾节点
        ListNode* newHead = reverseList(head->next);
        // 4. 回溯阶段:
        // 改变下个节点的指向为前一个
        // 并将本节点指向空,此处是为了将原始的头节点指向nullptr
        head->next->next = head;
        head->next = nullptr;
        return newHead;

    }
};

3.删除倒数第N个节点

力扣链接
题目描述:
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
递归法:从尾节点开始处理

class Solution {
public:
    // 递归从尾节点往前遍历删除
    // 需要保存当前是倒数第几个节点,count
    // 递推时传递0
    // 回溯时count++
    // 在待删除节点前一个位置,将该节点下一个删除,并更新next指向
    // 因此dfs递归函数参数采用指向指针的指针,来实现全局变量的效果
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        // 为了统一操作(只有一个节点时)添加一个虚拟头节点
        ListNode* vhead = new ListNode(0,head);
        // 计数指针
        int* count = new int(0);
        // 递归
        dfs(vhead,&count,n);
        delete count;
        return vhead->next;
    }
    void dfs(ListNode* cur,int** count,int n){
        // 1.递推结束条件:
        // 在尾节点下个nullptr处结束递推
        if(cur == nullptr)return;
        // 2. 进入递推,无需预处理
        dfs(cur->next,count,n);
        // 3. 回溯后处理,count计数++,判断是否是待删除节点前一个节点
        ++(*(*count));
        // 如果是待删除节点前一个,这时可以删除待删除节点
        if(*(*count) == n+1){
            ListNode* toDelete = cur->next;
            cur->next = cur->next->next;
            delete toDelete;
        }
        return;
    }

};

4.两两交换相邻节点

力扣链接
题目描述:
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
递归法:

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        // 1. 递推的终止条件
        if(head == nullptr || head->next == nullptr)return head;
        // 2. 递推之前的预处理
        // 需要保存当前节点的下个节点,以便在回溯时更改其next指向
        ListNode* newHead = head->next;
        // 3. 进入递推
        // 返回子区间的新的头节点,更新当前节点的next指向
        head->next = swapPairs(newHead->next);
        // 4. 回溯后处理
        // 更新当前节点的下个节点next指向自己,完成两个节点的互换
        newHead->next = head;
        return newHead;
    }
};

四、链表的基础操作:双指针

1.判断是否为环形链表

力扣链接
题目描述:
给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false 。
题目分析:
数组题目整理中的双指针法里面有道题已经分析了该问题,该题为寻找重复数,可以转化为有环链表的问题。
判断链表有环:非常简单,可以使用经典的龟兔赛跑法,也就是使用双指针(快慢指针)
慢指针一次走一个节点,快指针一次走两个节点;
这样快指针一定最先到达链表尾部。

  • 如果无环,则快指针指向nullptr,判断结束
  • 如果有环,则快指针会进入环中,无限循环,直到慢指针也进入环中,并最终相遇,此时即可判断出有环。
class Solution {
public:
    bool hasCycle(ListNode *head) {
        ListNode* vhead = new ListNode(0,head);
        ListNode* slow = vhead, *fast = head;
        while(fast != nullptr){
            slow = slow->next;
            if(fast->next == nullptr)return false;
            fast = fast->next->next;
            if(slow == fast)return true;
        }
        return false;
    }
};

2.找出环形链表的环入口位置

力扣链接
题目描述:
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表
题目分析:
那么如何找出环的入口呢?
使用数学方法推导:
在这里插入图片描述
考虑快慢指针相遇时:
有如下事实:

  1. 快指针走两步,慢指针走一步
  2. 快慢指针行进速度一致
  3. 因此快指针走过的结点数 = 2 * (慢指针走过的结点数)

慢指针走过的结点数: c + k 2 ( a + b ) + a c + k_2(a+b) + a c+k2(a+b)+a
快指针走过的结点数: c + k 1 ( a + b ) + a c + k_1(a+b) + a c+k1(a+b)+a
所以有 2 ( c + k 2 ( a + b ) + a ) 2(c + k_2(a+b) + a) 2(c+k2(a+b)+a) = c + k 1 ( a + b ) + a c + k_1(a+b) + a c+k1(a+b)+a
也即 c + a = ( k 1 − 2 k 2 ) ( a + b ) c+a = (k_1-2k_2)(a+b) c+a=(k12k2)(a+b)
整理得 c = ( k 1 − 2 k 2 − 1 ) a + ( k 1 − 2 k 2 ) b c=(k_1-2k_2-1)a+(k_1-2k_2)b c=(k12k21)a+(k12k2)b
换句话说 c = Q ( a + b ) + b c=Q(a+b) + b c=Q(a+b)+b
这个式子意味着,如果快指针从相遇点开始,慢指针从起点开始,两者每次都走一个节点,那么当慢指针从起点走到环的入口时,快指针也正好走到环的入口,两者相遇在环的入口。

因此,想要找出链表中还的入口位置:

  1. 使用快慢指针找到环中相遇点
  2. 此时让慢指针移动到起点
  3. 两者每次移动一个节点,找到相遇点,即为入口。
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode* slow = head, *fast = head;
        while(fast != nullptr){
            slow = slow->next;
            if(fast->next == nullptr)return nullptr;
            fast = fast->next->next;
            if(slow == fast)break;
        }
        if(fast == nullptr)return nullptr;
        // 有环,此时fast = slow,在环内某位置相遇
        // 为了找出环的入口,此时让slow = head,
        // 然后两者匀速前进,当slow 在此与 fast 相遇
        // 位置即为环的入口
        slow = head;
        while(slow != fast){
            slow = slow->next;
            fast = fast->next;
        }
        return slow;
    }
};

3.判断两个链表是否相交

力扣链接
题目描述:
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
题目分析:
使用双指针同时遍历两个链表,遍历完一个链表后遍历另一个,那么在这个过程中:

  • 如果有交点,两个指针一定能够相遇交点。
  • 如果没有交点,两个指针最终相遇于nullptr
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        if(headA == nullptr || headB == nullptr)return nullptr;
        ListNode* first = headA, * second = headB;
        while(true){
            if(first == second)return first;
            if(first == nullptr){
                first = headB;
                continue;
            }
            if(second == nullptr){
                second = headA;
                continue;
            }
            first = first->next;
            second = second->next;
        }
        return nullptr;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值