数据结构-链表-总结(缺递归)

前言:链表易混淆点:

1.node与node->val不要混淆,node表示整个节点,但是很多时候会错误理解为val

2.node->next只能指向一个地方,改变后之前的指针就没了

3.cur = head,然后操作cur来操作整个链表的原理不清楚:

cur = head这个操作相当于用一个指针变量指向head,但是这里的指向和next指针不同,这里是指一般的指针变量的指向的含义,也就是让cur指向与 head相同的内存位置,我们对cur的操作是可以改变head的

4.有时候会产生一个误解,比如代码中标注位置(二叉树模拟链表):

class Solution {
public:
    TreeNode* dummy = new TreeNode(0);
    //===============================
    TreeNode* cur = dummy->right;
    void traverse(TreeNode* root){
        if(!root) return;
        cur = new TreeNode(root->val);
        cur = cur->right;
    //================================    
        traverse(root->left);
        traverse(root->right);
    }
    void flatten(TreeNode* root) {
        if(!root)return;
        traverse(root);
        root->left = nullptr;
        root->right = dummy->right;
    }
};

我会以为cur先成为dummy的right节点,然后cur被修改为new值,再然后将cur向后移动

错误点:

1.我们不能只考虑next(此处为right),还应该考虑谁指向了当前指针,new值没有指针指向它

2.

一、链表基础操作(重要)

单链表有两种构造方式,一种是使用虚拟头节点,一种是不是虚拟头节点;

使用虚拟头节点可以使得对头节点操作和后面一致,因此后面都使用虚拟头节点创建;

1.1 创建链表节点

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

这样创建节点,可以在定义的时候初始化(输入val),更加方便

ListNode* head = new ListNode(5);

1.2 创建链表

    MyLinkedList() {
            _dummyhead = new LinkedNode(0);
            _size = 0;
    }

1.3 获取指定下标的元素值

后面会发现有些用的是cur = _dummyhead,而不是本题的cur = _dummyhead->next;这其实无所谓,只要搞清楚我们需要把指针移到哪里就行了,cur = _dummyhead会多移位一次而已;

    int get(int index) {
            LinkedNode* cur = _dummyhead->next;//要创建一个cur指针来指向要操作的位置,不知道直接操作头指针
            //
            if(index >= _size || index < 0) return -1;//最后一个节点是size-1
            while(index--){
                cur = cur->next;//cur指向向后移
            }
            return cur->val;
    }

1.4 头部添加节点(头节点之前添加)

添加节点的时候,指针要指向指定位置之前的节点,所有这里直接使用了虚拟头节点;

    void addAtHead(int val) {
            LinkedNode*   newnode = new LinkedNode(val);
            newnode->next = _dummyhead->next;
            _dummyhead->next = newnode;
            _size++;
    }

1.5 在链表尾部插入元素

主要考虑要移动多少次能够移动到最后一个节点;

    void addAtTail(int val) {
            LinkedNode* newnode = new LinkedNode(val);
            LinkedNode* cur = _dummyhead;
            while(cur->next != nullptr){
                cur = cur->next;
            }
            cur->next = newnode;
            _size++;
    }

1.6 在指定节点前面添加节点

因为是在前面添加,所以要操作的指针要指向目标节点的前面一个节点;

    void addAtIndex(int index, int val) {
            LinkedNode* newnode = new LinkedNode(val);
            LinkedNode* cur = _dummyhead;
            if(index > _size) return;
            if(index < 0 ) index = 0;
            while(index--){
                cur = cur->next;
            }
            newnode->next = cur->next;
            cur->next = newnode;//
            _size++;
    }

1.7 删除指定节点

要删除一个节点,也需要把要操作的指针指向目标节点之前;

    void deleteAtIndex(int index) {
            LinkedNode* cur = _dummyhead;
            if(index >= _size || index <0 ) return;//为什么是>=而不是>
            while(index--){
                cur = cur->next;
            }
            LinkedNode* tmp  = cur->next;
             cur->next = cur->next->next;
             delete tmp;
             tmp = nullptr;
             _size--;
    }

整个链表下:
 

class MyLinkedList {
public:
    struct LinkedNode{
            int val;
            LinkedNode* next;
            LinkedNode(int val): val(val),next(nullptr){}
    };

    MyLinkedList() {
            _dummyhead = new LinkedNode(0);
            _size = 0;
    }
    
    int get(int index) {
            LinkedNode* cur = _dummyhead->next;//要创建一个cur指针来指向要操作的位置,不知道直接操作头指针
            if(index >= _size || index < 0) return -1;//最后一个节点是size-1
            while(index--){
                cur = cur->next;//cur指向向后移
            }
            return cur->val;
    }
    
    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++;
    }
    
    void addAtIndex(int index, int val) {
            LinkedNode* newnode = new LinkedNode(val);
            LinkedNode* cur = _dummyhead;
            if(index > _size) return;
            if(index < 0 ) index = 0;
            while(index--){
                cur = cur->next;
            }
            newnode->next = cur->next;
            cur->next = newnode;//
            _size++;
    }
    
    void deleteAtIndex(int index) {
            LinkedNode* cur = _dummyhead;
            if(index >= _size || index <0 ) return;//为什么是>=而不是>
            while(index--){
                cur = cur->next;
            }
            LinkedNode* tmp  = cur->next;
             cur->next = cur->next->next;
             delete tmp;
             tmp = nullptr;
             _size--;
    }
private:
    int _size;
    LinkedNode* _dummyhead; 
};

一、(补充) 数组模拟链表 

1.单链表

首先我们总结使用链表需要的三个数据(尽管链表结构体只有两个参数,但实际需要注意的是三个,参考易混淆点部分),一个是值,一个是下一个元素的地址,最重要的一个是表示当前节点的变量

一般的 : 我们用e[]数组储存链表里面的值,用ne[]数组储存链表每个元素下一个元素的地址,用idx表示某一个节点,idx的数量表示了链表中节点数量,其值表示了某一个节点

注意:idx总是 == 当前节点数 + 1,这样保证每次新建的e【idx】和ne【idx】是新节点 

const int N = 1e6+10;

int e[N],ne[N],idx=1;   //因为有一个头节点,所以idx为一

ne[0]=-1;

 1.在第k个节点插入元素x

void to_insert(int k,int x)
{
	e[idx]=x,ne[idx]=ne[k],ne[k]=idx++;
    //注意顺序不能翻!!!
}

2.删除第k个节点后面的元素 

void remove(int k)
{
	ne[k]=ne[[k]];//直接让k<指向的地址>指向k+1<指向的地址>,即把k+1节点跳过
}

2.双链表 

 双链表的构成:

一个元素有四个数据,一个是元素的值,一个是下一个元素的地址,还有一个是上一个元素的地址,最后一个是当前节点,像这样的链表就是双链表

const int N = 1e6+10;

int e[N],l[N],r[N],idx;

void init()
{
    r[0]=1;l[1]=0;idx=2;
}

1.双链表在第k个节点右边插入元素x 

void to_insert(int k,int x)
{
	e[idx]=x,r[idx]=r[k],l[idx]=k,l[r[k]]=idx,r[k]=idx++;
}

 2.删除k节点的数

void remove(int k)
{
	l[r[k]]=l[k];r[l[k]]=r[k];
}

二、双指针

2.1 寻找倒数第N个节点

fast比slow多走N步即可;

代码实现:

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
            ListNode* dummy = new ListNode(0);
            dummy->next = head;
            ListNode* fast = dummy;
            ListNode* low = dummy;
            while(n--)
                fast = fast->next;
            while(fast->next != nullptr){
                fast = fast->next;
                low = low->next;
            }
            ListNode* tmp = low->next;
            low->next = low->next->next;
            delete tmp;
            tmp = nullptr;
            return dummy->next;
    }
};

2.2 链表相交

思路:先让长序列指针移动到和短序列相同位置,然后同时移动,知道指针相等;

注意:指针相等不是元素相等,指针相等时cur1==cur2,元素相等时cur1->val==cur2->val

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode* cur1 = headA;
        ListNode* cur2 = headB;
        int size1 = 0;
        int size2 = 0;
        ListNode* nul = new ListNode(0);
        nul = nullptr;
        while(cur1 !=nullptr){
            cur1 = cur1->next;
            size1++;
        }
        while(cur2 !=nullptr){
            cur2 = cur2->next;
            size2++;
        }
        cur1 = headA;
        cur2 = headB; 
        if(size1 >= size2){
            int k = size1 - size2;
            while(k--) cur1 = cur1->next;
            while(cur1 != cur2){
                cur1 = cur1->next;
                cur2 = cur2->next;
            }
        }else{
            int k = size2 - size1;
            while(k--) cur2 = cur2->next;
            while(cur1 != cur2){
                cur1 = cur1->next;
                cur2 = cur2->next;
            }
        }

        if(cur1 == nullptr) return nul;
        else return cur1;


    }
};

2.3 快慢指针(一倍数和两倍速)

1. 寻找单链表的中点

使用一个快指针(2倍数)一个慢指针(1倍数),快指针指到尾端时,慢指针指向中点

代码实现:

lass Solution {
public:
    ListNode* middleNode(ListNode* head) {
        ListNode* fast = head, *slow = head;
        while(fast != nullptr && fast->next != nullptr){
            slow = slow->next;
            fast = fast->next->next;
        }
        return slow;
    }
};

2. 判断单链表是否包含环并找出环起点

可以使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。

fast指针先移动,移到环里面时就一直沿着环循环,slow在后面,最终slow和fast在环内相遇,

那么相遇时: 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同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。

代码实现:

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
         ListNode* fast = head;
         ListNode* low = head;
         ListNode* nul = new ListNode(0);
         nul = nullptr;
         int n = 0;
         while(fast != nullptr && fast->next != nullptr){//fast每次跳两步,因此要判断fast->next
             fast = fast->next->next;
             low = low->next;
             if(fast == low) {
                    ListNode* cur1 = fast;
                    ListNode* cur2 = head;
                    while(cur1 != cur2){
                        cur1 = cur1->next;
                        cur2 = cur2->next;
                    } 
                    return cur1;
             }//不要在这里写else然后return,不然每次指针移动都会导致返回空
         }
         return nul;
    }
};

2.4 合并分解链表

1.合并两个链表

创建一个新链表存放即可

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        ListNode* cur1 = list1;
        ListNode* cur2 = list2;
        ListNode* dummyhead = new ListNode(0),*p = dummyhead;
        while(cur1 != nullptr && cur2 != nullptr){
            if(cur1->val > cur2->val){
                p->next = cur2;
                p = p->next;
                cur2 = cur2->next; 
            }else{
                p->next = cur1;
                p = p->next;
                cur1 = cur1->next; 
            }
        }
        if(cur1 != nullptr){
            p->next = cur1;
        }
        if(cur2 != nullptr){
            p->next = cur2;
        }
        return dummyhead->next;
    }
};

2. 分解两个链表

注意要保持相对位置不变

我们创建两个链表,一个存放大于等于x的数,一个存放小于x的数,然后将两个链表合并就行了

链表的连接参考合并链表,但是要注意最后两个链表都链接结束后尾端会链接在一起,需要把队尾置为null

lass Solution {
public:
    ListNode* partition(ListNode* head, int x) {
        ListNode* p = head;
        ListNode* dummyhead1 = new ListNode(0),*p1 = dummyhead1;
        ListNode* dummyhead2 = new ListNode(0),*p2 = dummyhead2;
        while(p != nullptr){
            if(p->val >= x){
                p1->next = p;
                p1 = p1->next;
                p = p->next; 
                // p1->next = nullptr;//要将指针置为空,防止产生环
            }else{
                p2->next = p;
                p2 = p2->next;
                p = p->next;
                // p2->next = nullptr;
            }

        }
        p1->next = nullptr;//在这里置为空也可以
        p2->next = nullptr;
        p2->next = dummyhead1->next;
        return dummyhead2->next;
    }
};

3. 合并 k 个有序链表

优先级队列,详情见堆栈章节

2.5 反转链表(双指针法:推荐)

首先我们呀知道反转整个链表的操作,部分操作如下图,具体操作见代码随想录 反转链表

那么对于反转链表指定区间,可以将指针先索引到要操作的区间开头,然后利用反转整个链表的思路反转中间部分,最后将前后与中间反转过的部分相连接

代码实现:

class Solution {
public:
    ListNode* reverseBetween(ListNode* head, int left, int right) {
        ListNode* dummyhead = new ListNode(0);
        dummyhead->next = head;
        ListNode* cur1 =dummyhead,*cur2=dummyhead;
        int l = left-1,k=right-left;
        while(l--){
            cur1 = cur1->next;
            cur2 = cur2->next;
        }
        ListNode* cur = cur1;
        ListNode* curb = cur1->next;
        cur1 = cur1->next;
        cur2 = cur2->next->next;
        while(k--){
            ListNode* tmp = cur2->next;
            cur2->next = cur1;
            cur1 = cur2;
            cur2 = tmp;
        }
        cur->next = cur1;
        curb->next = cur2;
        return dummyhead->next;
    }
};

2.6 回文链表

可以先将指针移到链表中间,然后反转后半部分用来和前半部分比较

代码实现:

class Solution {
public:
    bool isPalindrome(ListNode* head) {
        ListNode*fast=head,*slow=head;
        //令slow指向中点
        while(fast!=nullptr && fast->next != nullptr){
            fast = fast->next->next;
            slow = slow->next;
        }   
        //fast从起点开始
        fast = head;
        //使用双指针反转slow之后的部分链表,创建新指针moreslow
        ListNode* moreslow = slow;
        //为了在反转的第一步产生环,需要先把moreslow指向nullptr,但是要注意先把slow移动到后面,不然moreslow指向nullptr后,slow就找不到后面在哪了
        slow = slow->next;
        moreslow->next = nullptr;
        //反转链表
        while(slow != nullptr){
            ListNode* tmp = slow->next;
            slow->next = moreslow;
            moreslow = slow;
            slow = tmp; 
        }
        //比较是否回文
        while(moreslow != nullptr){
            if(moreslow->val != fast->val) return false;
            moreslow = moreslow->next;
            fast = fast->next;
        }
        return true;
    }
};

三、非双指针算法

3.1 两两交换链表中的节点

例:

初始时,cur指向虚拟头结点,然后进行如下三步:

操作之后,链表如下:

然后让cur指向->next()->next();

代码实现:

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        ListNode*  dummy = new ListNode(0);
        dummy->next = head;
        ListNode* cur = dummy;
        while(cur->next != nullptr && cur->next->next != nullptr){
            //保存两个节点的值防止覆盖
            ListNode* node1 = cur->next;
            ListNode* node2 = cur->next->next->next;
            //注意按照123顺序防止覆盖
            cur->next = node1->next;
            cur->next->next = node1;
            node1->next = node2;
            //然后将指针后移两位
            cur = cur->next->next;
        }
        return dummy->next;
    }
};

3.2 反转链表(递归法)

等后面学会递归了再来总结这部分

1.反转整个链表

2.反转部分链表

3.k个一组反转链表
 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值