链表理论基础

1.链表理论基础

什么是链表,链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)
链表的入口节点称为链表的头结点也就是head。
在这里插入图片描述

1.1链表的类型

单链表

上述就是一个单链表,单链表中的指针域只能指向节点的下一个节点。

双链表

双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。

双链表 既可以向前查询也可以向后查询。如下:
在这里插入图片描述

循环链表

循环链表,顾名思义,就是链表首尾相连。
在这里插入图片描述

1.2链表的存储方式

数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。
链表是通过指针域的指针链接在内存中各个节点。
所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
在这里插入图片描述
这个链表起始节点为2, 终止节点为7, 各个节点分布在内存的不同地址空间上,通过指针串联在一起。

1.2链表的定义

给出C/C++的定义链表节点方式,如下所示

// 单链表
struct ListNode {
	int val; 		 //节点上的数据域
	ListNode* next;  //指针域:指向下一个节点
	// 节点的构造函数,可以不写,系统默认生成,但是不写无法定义构造初始化节点
	ListNode(int x): val(x),next(NULL)
	{}
};

通过自己定义构造函数初始化节点:

ListNode* head = new ListNode(5);

使用默认构造函数初始化节点:

ListNode* head = new ListNode();
head->val = 5;

所以如果不定义构造函数使用默认构造函数的话,在初始化的时候就不能直接给变量赋值!

1.3链表的操作

删除结点

删除D节点,如图所示:
在这里插入图片描述
只要将C节点的next指针 指向E节点就可以了。

那有同学说了,D节点不是依然存留在内存里么?只不过是没有在这个链表里而已。

是这样的,所以在C++里最好是再手动释放这个D节点,释放这块内存

其他语言例如Java、Python,就有自己的内存回收机制,就不用自己手动释放了。

添加结点

现要将F插入到C与D之间。首先要先让C->next从指向D断开变为指向F,C->next = F,
再让F中的指针域指向的下一个节点变为D,F->next = D,
对于单向链表如果是头插,dummyHead->next = F, F->next = A 即可
对于单向链表如果是尾插,E->next = F, F->next = NULL即可
对于循环链表就和插入在两个节点中间的操作一样
在这里插入图片描述
可以看出链表的增添和删除都是O(1)操作,也不会影响到其他节点。

但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。

1.4性能分析

在这里插入图片描述
数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。

链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。

2.移除链表中的元素

链表操作中,可以使用原链表来直接进行删除操作,也可以设置一个虚拟头结点在进行删除操作,接下来看一看哪种方式更方便。

203.移除链表元素

力扣题目链接

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
在这里插入图片描述

示例 1:
输入:head = [1,2,6,3,4,5,6],
val = 6 输出:[1,2,3,4,5]

示例 2:
输入:head = [],
val = 1 输出:[]

示例 3:
输入:head = [7,7,7,7],
val = 7 输出:[]

思路

这里以链表 1 4 2 4 来举例,移除元素4。
在这里插入图片描述
说明一下,就算使用C++来做leetcode,如果移除一个节点之后,没有手动在内存中删除这个节点,leetcode依然也是可以通过的,只不过,内存使用的空间大一些而已,但建议依然要养成手动清理内存的习惯。

这种情况下的移除操作,就是让节点next指针直接指向下下一个节点就可以了
那么因为单链表的特殊性,只能指向下一个节点,刚刚删除的是链表的中第二个,和第四个节点,那么如果删除的是头结点又该怎么办呢?

这里就涉及如下链表操作的两种方式:

  • 直接使用原来的链表来进行删除操作。
  • 设置一个虚拟头结点在进行删除操作。

来看第一种操作:直接使用原来的链表来进行移除。
在这里插入图片描述移除头结点和移除其他节点的操作是不一样的,因为链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。所以只要将头结点向后移动一位就可以了
在这里插入图片描述依然别忘将原头结点从内存中删掉

在这里插入图片描述

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {  
        // 头结点节点的删除
        // 这里不是if而是while 如果是 1 1 1 1 恰好又要删除1,用if就只能删掉第一个1
        while (head != NULL && head->val == val){
            //注意创建这个指针的目的是为了让其指向待删除的节点内存,节点删除时,将被删的节点内存释放掉
            ListNode* tmp = head;
            head = head->next;
            delete tmp;// 释放掉tmp指向的全部内存空间
        }
       
        // 删除非头结点
        // 这里必须要一个零时指针变量来不断接收变化的head,如果直接用head,最后return的值并不指向最开始
        ListNode* cur = head;
        // 要删除的并不是cur,而是检测cur->next是否满足条件
        while (cur != NULL && cur->next != NULL){ 
            if (cur->next->val == val){
                ListNode* tmp = cur->next;
                cur->next = cur->next->next;
                delete tmp;
            } 
            else
                cur = cur->next;
        }  

        return head;
    }
};

其实可以设置一个虚拟头结点dummyNode使其指针域存放的是头结点的地址,这样原链表的所有节点就都可以按照统一的方式进行移除了。

在这里插入图片描述
最后呢在题目中,return 头结点的时候,别忘了 return dummyNode->next;, 这才是新的头结点

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        ListNode* dummyHead =  new ListNode(0); //设置一个虚拟头结点
        dummyHead->next = head;
        // 这里必须要一个零时指针变量来不断接收变化的dummyHead,如果直接用dummyHead,后面指针会将其移动
        ListNode* cur = dummyHead; 
        
        // 一直遍历完所有的节点
        while (cur->next != NULL){
            if ( cur->next->val == val){
            	 // 用来释放删除结点的空间
            	ListNode* tmp = cur->next;
            	 // 找到待删除的元素,将下下个元素的地址赋给cur->next
                cur->next = cur->next->next;
                delete tmp;
            }
            else
                cur = cur->next;// 不是当前的节点,当前指针后移往后找
        }
        head = dummyHead->next;
        delete dummyHead;
        return head;
    }
};

707.设计链表
在链表类中实现这些功能:

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

addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
在这里插入图片描述
这道题目设计链表的五个接口:

  • 获取链表第index个节点的数值
  • 在链表的最前面插入一个节点
  • 在链表的最后面插入一个节点
  • 在链表第index个节点前面插入一个节点
  • 删除链表的第index个节点

可以说这五个接口,已经覆盖了链表的常见操作,是练习链表操作非常好的一道题目

链表操作的两种方式:

  • 直接使用原来的链表来进行操作。
  • 设置一个虚拟头结点在进行操作。

下面采用的设置一个虚拟头结点(这样更方便一些,大家看代码就会感受出来)

class MyLinkedList {
public:
    // 定义链表节点结构体
    struct LinkedNode {
        int val;
        LinkedNode* next;
        LinkedNode(int val) :val(val), next(nullptr) {}
    };


     //初始化链表,在链表中先建立一个虚拟头部的,并非真正的头结点
    MyLinkedList() {
        // 为了dummyHead和size所有内置方法都可以使用,需要把它们定义为内置属性
        dummyHead = new LinkedNode(0);
        size = 0;
    }

  

    // 获取到第index个节点数值,如果index是非法数值直接返回-1, 注意index是从0开始的,第0个节点就是头结点
    int get(int index) {
        if (index >= size || index < 0)           
            return -1;

        LinkedNode* cur = dummyHead->next;          // 以dummyHead->next为起始遍历节点,出循环时恰好找到的是第index个节点
        while (index) {
            cur = cur->next;                        // 可写成while(index--)
            index--;
        }
        return cur->val;
    }



    // 在链表最前面插入一个节点,插入完成后,新插入的节点为链表的新的头结点
    void addAtHead(int val) {
        LinkedNode* newNode = new LinkedNode(val);   // 建立一个新的头部节点
        newNode->next = dummyHead->next;             // 原虚拟头部指向的旧头结点变为新头结点的下一节点
        dummyHead->next = newNode;                   // 让虚拟头部指向它
        size++;                                      // 插入成功,链表元素+1
    }

    // 在链表最后面添加一个节点
    void addAtTail(int val) {
        LinkedNode* newNode = new LinkedNode(val);
        LinkedNode* cur = dummyHead;
        // 找原链表中最后一个节点
        while (cur->next != NULL){
            cur = cur->next;     
        }
        cur->next = newNode;
        size++;
    }



    // 在第index个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
    // 如果index 等于链表的长度,则说明是新插入的节点为链表的尾结点
    // 如果index大于链表的长度,则返回空
    void addAtIndex(int index, int val) {
        if (index > size) {
            return;
        }
        LinkedNode* newNode = new LinkedNode(val);
        /*既然是在index之前插入,就需要找到第index个节点的前一个节点,如果这里用cur = dummyHead->next,
        则找到的cur是第index个位置,单链表无法获取到它上一个节点的指针域*/
        LinkedNode* cur = dummyHead;
        while(index--) {
            cur = cur->next;
        }
        newNode->next = cur->next;  // cur为index的前一个节点,cur->next为第index个节点
        cur->next = newNode;
        size++;
    }


    // 删除第index个节点,如果index 大于等于链表的长度,直接return,注意index是从0开始的
    void deleteAtIndex(int index) {
        if (index >= size || index < 0) {
            return;
        }
        // 开始挨个寻找待删除的节点的前一个节点 ,注意和上面写法的区别
        LinkedNode* cur = dummyHead;
        while(index){
            cur = cur->next;
            index--;
        }
        // 跳出循环说明cur是第index的前一个节点地址
        LinkedNode* tmp = cur->next;
        cur->next = cur->next->next;   
        size--;
    }


    // 打印链表
    void printLinkedList() {
        LinkedNode* cur = dummyHead;
        while (cur->next != nullptr) {
            cout << cur->next->val << " ";
            cur = cur->next;
        }
        cout << endl;
    }
private:
    int size;
    LinkedNode* dummyHead;

};

3.反转链表

206.反转链表
在这里插入图片描述
示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL

思路

  • 改变链表的next指针的指向,直接将链表反转

    双指针法
    首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。

    为什么要保存一下这个节点呢,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。
 // 只需要改变链表的next指针的指向,直接将链表反转,而不用重新定义一个新链表
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* tmp = NULL;  //创建一个零时指针用于接收节点调方向以前的下一个地址
        ListNode* cur =  head;
        ListNode* pre = NULL;  //创建前置指针
        
        // 遍历链表,让它们挨个往前指
        while (cur){
            tmp = cur->next;  //原来顺序的下一个节点,因为接下来要改变cur->next
            cur->next =  pre; //指向前一个元素

            pre = cur;        //当前的cur成为下一个节点需要指向的前驱节点
            cur = tmp;        //将指针后移
        }

        return pre; //pre指向的是反转后第一个节点
    }
};

4.两两交换链表中的节点

在这里插入图片描述
初始时,cur指向虚拟头结点,然后进行如下三步
请添加图片描述
请添加图片描述

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        ListNode* dummyhead = new ListNode(0);
        dummyhead->next = head;
        ListNode* cur = dummyhead;

        //至少要保证链表里有两个节点才能做交换, cur->next->next要存在
        while (cur->next != NULL && cur->next->next != NULL){ 
            ListNode* Node1 = cur->next;                //存放原先前面一个节点的地址
            ListNode* Node2 = cur->next->next->next;;   //存放原先后面一个节点的下一个地址

            cur->next = Node1->next;                    //  步骤一 将头节点换成原来头的下一个节点,新头结点变为Node1->next
            Node1->next->next = Node1;                  //  步骤二 为其下一个节点地址赋值,原来的头结点
            Node1->next = Node2;                        //  步骤三 

            cur = cur->next->next;                      // cur移动两位,准备下一轮交换
        }
        return dummyhead->next;
    }
};

5.删除链表的倒数第N个节点

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
进阶:你能尝试使用一趟扫描实现吗?

在这里插入图片描述
输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5] 示例 2:

输入:head = [1], n = 1 输出:[] 示例 3:

输入:head = [1,2], n = 1 输出:[1]

#思路
法一:双指针的经典应用,如果要删除倒数第n个节点,让fast移动n+1步,然后让fast和slow同时移动,直到fast指向链表末尾NULL, 此时slow所指向的节点是待删除的前一个结点。

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummyHead = new ListNode(0);
        dummyHead->next = head;
        ListNode* slow = dummyHead;
        ListNode* fast = dummyHead;
        while(n-- && fast != NULL) {
            fast = fast->next;
        }
        fast = fast->next; // fast再提前走一步,因为需要让slow指向删除节点的上一个节点
        
        while (fast != NULL) {
            fast = fast->next;
            slow = slow->next;
        }
        slow->next = slow->next->next;
        return dummyHead->next;
    }
};

法二:(设index为链表倒数第n个结点,正数的标志) 总是存在 index + n = size , 所以也就是删除链表正数第 size - n 个结点

class Solution {
public:
    int getLength(ListNode* head){
        if (head == NULL) 
            return 0;
        ListNode* cur = head;
        int length = 0;
        while (cur!= NULL){
            length++;
            cur = cur->next;
        }
        return length;
    }

    ListNode* removeNthFromEnd(ListNode* head, int n) {
        // 正着数index从0开始,index + n =  size,即删除倒数第n个就是删除正数的第size - n个
        // 先创建虚拟头结点,并计算链表的长度
        ListNode* dummyHead = new ListNode(0);
        dummyHead->next = head;
        ListNode* cur = dummyHead;
        int size = getLength(head);
        int index = size - n;

        // 找到第size-n个元素的前一个元素
        while (index--){
            cur = cur->next;
        }
        ListNode* tmp = cur->next;
        cur->next = cur->next->next;
        delete tmp;

        return dummyHead->next; 
    }
};

6.链表相交

面试题02.07.链表相交
在这里插入图片描述

思路

简单来说,就是求两个链表交点节点的指针。 这里同学们要注意,交点不是数值相等,而是指针相等。

为了方便举例,假设节点元素数值相等,则节点指针相等。

看如下两个链表,目前curA指向链表A的头结点,curB指向链表B的头结点:
在这里插入图片描述

我们求出两个链表的长度,并求出两个链表长度的差值,然后让curA移动到,和curB 末尾对齐的位置,如图:在这里插入图片描述

此时我们就可以比较curA和curB是否相同,如果不相同,同时向后移动curA和curB,如果遇到curA == curB,则找到交点。
否则循环退出返回空指针

class Solution {
public:
    int getLength(ListNode *head){
        if (head == NULL) return 0;
        int len= 0;
        ListNode* cur = head; 
        while (cur != NULL){
            len++;
            cur = cur->next;
        }
        return len;
    }

    ListNode* getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode* curA = headA;
        ListNode* curB = headB;
        int lenA = getLength(headA);
        int lenB = getLength(headB);
       
        if (lenB > lenA){                 // 让curA指向最长的链表的头部,lenA为其长度
            swap(lenA, lenB);
            swap(curA, curB);
        }
      
        int gap = lenA - lenB;           //长度差
        while (gap--) {                  // 让curA和curB在同一起点上(末尾位置对齐)
            curA = curA->next;
        }
   
        while (curA != NULL) {           // 遍历curA 和 curB,现在它们一样长,遇到相同则直接返回
            if (curA == curB) {
                return curA;
            }
            curA = curA->next;
            curB = curB->next;
        }
        return NULL;
    }
};

7.两两交换链表中的节点

题意: 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

说明:不允许修改给定的链表。
在这里插入图片描述
思想

  • 首先要判断链表有没有环,如何判断?用快慢指针,让快指针fast每次走两步,慢指针slow走一步,如果有环,因为fast是走两步,slow是走一步,其实相对于slow来说,当其进入环以后,fast是以相对速度每次一个节点的距离靠近slow,所以fast一定可以和slow在环内某个地方重合。
  • 如果有环,如何找到这个环的入口?
  • 假设从头结点到环形入口节点 的节点数为x。环形入口节点到 fast指针与slow指针相遇节点节点数为y。 从相遇节点再到环形入口节点节点数为 z。 如图所示:

在这里插入图片描述
那么相遇时: slow 指针走过的节点数为: x + y,
         fast 指针走过的节点数:x + y + n (y + z),n为fast指针在环内走了n圈才遇到slow指针,
        (y+z)为 一圈内节点的个数A。

fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2 (x + y) * 2 = x + y + n (y + z)

两边消掉一个(x+y): x + y = n (y + z) 因为要找环形的入口,那么要求的是x,因为x表示头结点到 环形入口节点的的距离。
所以要求x ,将x单独放在左面:x = n (y + z) - y , 再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:
x = (n - 1) (y + z) + z 注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。
这个公式说明什么呢? 先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了slow指针了。 当
n为1的时候,公式就化解为 x = z,**

这就意味着,从头结点出发一个指针,从相遇节点也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是环形入口的节点。

也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。

让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。

当n>1的i情况是一样的,无非是fast在环里多环了几圈,最终它们的相遇点依然是环形入口的节点

动画如下:

环形链表

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode* fast = head; 
        ListNode* slow = head;
        
        while (fast != NULL && fast->next != NULL ){
            slow = slow->next;
            fast = fast->next->next;

            // 当快慢指针相遇时
            if (slow == fast){
                 ListNode* index1 = head; 
                 ListNode* index2 = fast; //相遇点,两个指针重合,就是fast当前的指针
                 
                 //此时从head和相遇点,同时查找直至相遇
                 while (index1 != index2){
                     index1 = index1->next;
                     index2 = index2->next;
                 }
                 return index1;
            }
        }
        return NULL;      
    }
};

8.总结

在这篇文章关于链表,介绍了如下几点:

链表的种类主要为:单链表,双链表,循环链表
链表的存储方式:链表的节点在内存中是分散存储的,通过指针连在一起。
链表是如何进行增删改查的。
数组和链表在不同场景下的性能分析。
可以说把链表基础的知识都概括了,但又不像教科书那样的繁琐。

虚拟头结点

在链表:讲解了链表操作中一个非常总要的技巧:虚拟头节点
链表的一大问题就是操作当前节点必须要找前一个节点才能操作。这就造成了,头结点的尴尬,因为头结点没有前一个节点了。每次对应头结点的情况都要单独处理,所以使用虚拟头结点的技巧,就可以解决这个问题。

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值