第四章 链表

4.1 代码定义链表

        根据链表的定义可知,链表的一个节点包含节点值val和指向下一个节点的指针两个元素,并且在设计时需要他们设计成一个整体。故需要使用结构体或者类进行设计,因为没有私有元素的操作,结构体就可以满足需求。结构体是特殊的类,其所有的结构体变量和结构体函数都是默认的为pubilc类型。具体的实现代码如下:

struct List Node{
    int val;
    ListNode * Next;
    ListNode(int x) : val(x),Next(nullptr){}
//这里使用了初始化列表,val(x)就等价于 val = x;(但是程序的执行时间的不同)
//构造函数的初始化列表的解释:https://blog.youkuaiyun.com/xzli8_geo/article/details/85037530
};

为什么初始化的顺序是变量的声明顺序?我觉得是C++语言自己就固定好,为了避免初始化程序时出现错误提前定死的。在构造函数里面,初始化列表的变量初始化顺序与列表中变量的排列的顺序无关,是与类或者结构体中,变量的声明顺序一致。当不满足上面的一致性要求时就会出现,下图的情况:

4.2 链表的六个操作

4.2.1 虚拟头节点

        虚拟头节点就是指在原头节点(含有实际值和下一个节点的地址)的前面加上一个不含实际值,但是包含了下个节点地址的链表节点。下图为添加了虚拟节点的链表:

        添加虚拟节点是为了解决编写节点删除程序时需要单独处理头节点的问题,提高代码效率。因为在没有头节点的链表中,头节点的值无法由上一个节点进行链接获取。

        这个问题对应了力扣题的203题。主要实现步骤是(1)创建一个新的链表节点(2)将新的链表节点指向头节点,即Next = 头节点的地址(3)读取下一个节点的值并判断是否需要删除(4)直至删除完成。解决代码如下:

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点
        dummyHead->next = head; // 将虚拟头结点指向head,这样方面后面做删除操作
        ListNode* cur = dummyHead;
        while (cur->next != NULL) {  
            if(cur->next->val == val) { //读取下一个节点的值,而不是当前节点的值
                ListNode* tmp = cur->next;
                cur->next = cur->next->next; //将当前节点Next指针指向下下个节点的地址
                delete tmp; //c++代码必须手动删除多余的空间,不然会代码泄露
            } else {
                cur = cur->next; //遍历操作
            }
        }
        head = dummyHead->next; //还原链表
        delete dummyHead; //删除申请的动态空间
        return head;
    }
};

4.2.2 获取某个节点的值

        这个问题涉及链表得遍历问题,使用的是cur = cur->next;然后值得一提的这里的某个节点,也就是第几个节点,数值是从0开始数的。实现的代码如下:

    // 获取到第index个节点数值,如果index是非法数值直接返回-1, 注意index是从0开始的,第0个节点就是头结点
    int get(int index) { 
            return -1;
        }
        LinkedNode* cur = _dummyHead->next;
        while(index--){ // 如果--index 就会陷入死循环。当index为0时,index会先变成-1然后再判断,由于非0即1,所以进入循环,如此往后会恒为负数恒为真,就会导致无限循环
           //这里的index的是从0开始计数的,如第二个,应该代表的是实际的第三个节点
        if (index > (_size - 1) || index < 0) {
            cur = cur->next;
        }
        return cur->val;
    }

4.2.3 插入节点

        插入节点分为三个类型,在头部插入,在尾部插入,在中间插入(某个节点的前面)。原理很简单,直接看代码即可。

    // 在链表最前面插入一个节点,插入完成后,新插入的节点为链表的新的头结点
   //注意从始至终都有虚拟头节点的存在
    void addAtHead(int val) {
        LinkedNode* newNode = new LinkedNode(val);
        newNode->next = _dummyHead->next;
        _dummyHead->next = newNode;
        _size++;
    }

    // 在链表最后面添加一个节点
    void addAtTail(int val) {
        LinkedNode* newNode = new LinkedNode(val);
        LinkedNode* cur = _dummyHead;
        while(cur->next != nullptr){
            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);
        LinkedNode* cur = _dummyHead;
      //注意这里使用的是_dummyHead而不是_dummyHead->next,因为要删除一个节点,必须要去到他的前一个节点才可以完成。在获取某一个节点的值时,需要到达指定节点,故是_dummyHead->next
        while(index--) {
            cur = cur->next; //遍历
        }
        newNode->next = cur->next; //将新节点指向第index个节点
        cur->next = newNode;//在index前添加新节点
        _size++;
    }

4.2.4 删除节点

        这里的删除节点指的是删除第几个节点。实现代码如下,与插入节点的方法差不多

    // 在链表最前面插入一个节点,插入完成后,新插入的节点为链表的新的头结点
    void addAtHead(int val) {
        LinkedNode* newNode = new LinkedNode(val);
        newNode->next = _dummyHead->next;
        _dummyHead->next = newNode;
        _size++;
    }

    // 在链表最后面添加一个节点
    void addAtTail(int val) {
        LinkedNode* newNode = new LinkedNode(val);
        LinkedNode* cur = _dummyHead;
        while(cur->next != nullptr){
            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);
        LinkedNode* cur = _dummyHead; //这里是_dummyHead,不是_dummyHead->next
        while(index--) {
            cur = cur->next;
        }
        newNode->next = cur->next;
        cur->next = newNode;
        _size++;
    }

4.2.5 打印链表

        打印链表主要注意的是链表是如何进行遍历的。在读取实际值时,使用的上一个节点的next获得,因为遍历终止的条件是节点的next为空。具体实现如下:

    void printLinkedList() {
        LinkedNode* cur = _dummyHead;
        while (cur->next != nullptr) {
            cout << cur->next->val << " ";
            cur = cur->next;
        }
        cout << endl;
    }

4.3 反转链表

        反转链表的目的是将链表的指向顺序一个方向转换的反方向,这个问题对应力扣题的206题。解决方法有两个,双指针(其实我觉得有三个指针在参与活动)和递归法。实现效果如图:

4.3.1 双指针法

        其实觉得实质就是将用三个指针分别保存 当前节点地址(cur),下一个节点地址(temp),前一个节点的地址(pre)

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* temp; // 保存cur的下一个节点
        ListNode* cur = head;
        ListNode* pre = NULL;
        while(cur) {
            temp = cur->next;  // 保存一下 cur的下一个节点,因为接下来要改变cur->next
            cur->next = pre; // 翻转操作
            // 更新pre 和 cur指针
            pre = cur;
            cur = temp;
        }
        return pre;
    }
};

4.3.2 递归法 

        原理和双指针法是一样的,它是将循环里面的操作封装成了函数就行层层递归。这里return reverse(cur,temp);为什么可以这样子进行返回,因为函数reverse的返回值是ListNode*符合函数的返回值,这里是将最后一层的结果往上进行返回。总之条件是下一个节点是空节点。

class Solution {
public:
    ListNode* reverse(ListNode* pre,ListNode* cur){
        if(cur == NULL) return pre;
        ListNode* temp = cur->next;
        cur->next = pre;
        // 可以和双指针法的代码进行对比,如下递归的写法,其实就是做了这两步
        // pre = cur;
        // cur = temp;
        return reverse(cur,temp);
   //    这相当于把最深层递归的返回值(新头节点)层层传递到最外层
  //  最终 reverseList 得到的就是这个传递上来的新头节点
    }
    ListNode* reverseList(ListNode* head) {
        // 和双指针法初始化是一样的逻辑
        // ListNode* cur = head;
        // ListNode* pre = NULL;
        return reverse(NULL, head);
    }

};

4.4 删除倒数第n个节点

        这里删除节点比前面删除节点要麻烦些,因为我无法直接找到最后一个节点的地址,然后往前进行删除。在解决这个问题时候,要知道链表的终止条件是cur->next是为NULL,如何才能将满足终止条件的情况下完成题目要求?有一个想法是将n的长度做成一个窗口,然后每一次整个移动窗口,然后当窗口的右边碰到结尾时就可以找到指定位置了。但是为了可以实现删除,需要找到倒数第n个节点的前一个节点的,所以窗口的长度是n+1.窗口的实现与第三章的滑动窗口上实现原理相同,双指针。实现代码如下:

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; 
        
        // ListNode *tmp = slow->next;  C++释放内存的逻辑
        // slow->next = tmp->next;
        // delete tmp;
        
        return dummyHead->next;
    }
};

4.5 环形链表

       此问题对应力扣题的142题。这个题目需要解决两个问题,是否有环?如何找到环的第一个节点?判断是否有环可以直接用两个指针在带链表中进行遍历,快指针比慢指针的运动速度快1,这个当快指针进入环之后一定会与慢指针相遇。如果不相遇就一定不会有环。寻找头节点的数学分析如下:需要注意的是,快指针至少需要在环内运动一周之后才会与慢指针相遇。

        实现代码如下:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
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;
            // 快慢指针相遇,此时从head 和 相遇点,同时查找直至相遇
            if (slow == fast) { //当找到相遇节点后,然后快慢指针在走一段,使之再次相遇。相遇点即为环的开始节点
                ListNode* index1 = fast;//快指针从相遇点开始运动。
                ListNode* index2 = head;//慢指针从头开始运动
                while (index1 != index2) {
                    index1 = index1->next;
                    index2 = index2->next;
                }
                return index2; // 返回环的入口
            }
        }
        return NULL;
    }
};

        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值