代码随想录Day2

今天主要练习链表相关的题,练习时特别重要的的一个点在于画图,画图是最容易让人理解链表操作的方式。笔者在最初学习C++时观看的网课,当时的老师就是通过画图的方式非常清晰地讲解了链表、队列等的各种操作,为自己如今的理解奠定了良好的基础。

707.设计链表

题目:设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val 和 next。val 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。

在链表类中实现这些功能:

get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val  的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。

示例:

MyLinkedList linkedList = new MyLinkedList();
linkedList.addAtHead(1);
linkedList.addAtTail(3);
linkedList.addAtIndex(1,2);   //链表变为1-> 2-> 3
linkedList.get(1);            //返回2
linkedList.deleteAtIndex(1);  //现在链表是1-> 3
linkedList.get(1);            //返回3

思路:

1.这一题是很经典的初学数据结构便会接触的,手敲链表实现各种操作。

2.引入虚拟头结点,这样做的好处是对于涉及操作链表头结点的时候更加方便。

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

    MyLinkedList(){
        preHead = new ListNode(0);
        size = 0;
    }

    int get(int index){
        if(index < 0 || index > size - 1){
            return -1;
        }
        ListNode* cur = preHead;
        while(index--){
            cur = cur->next;
        }
        return cur->next->val;
    }

    void addAtTail(int val){
        ListNode* cur = preHead;
        while(cur->next){
            cur = cur->next;
        }
        ListNode* newNode = new ListNode(val);
        cur->next = newNode;
        size++;
    }

    void addAtHead(int val){
        ListNode* newNode = new ListNode(val);
        newNode->next = preHead->next;
        preHead->next = newNode;
        size++;
    }

    void addAtIndex(int index,int val){
        if(index == size) addAtTail(val);
        else if(index > size) return;
        else if(index < 0) addAtHead(val);
        else{
            ListNode* cur = preHead;
            while(index--){
                cur = cur->next;
            }
            ListNode* newNode = new ListNode(val);
            newNode->next = cur->next;
            cur->next = newNode;
            size++;
        }
    }

    void deleteAtIndex(int index){
        if(index < 0 || index > size - 1){
            return;
        }
        ListNode* cur = preHead;
        while(index--){
            cur = cur->next;
        }
        ListNode* tmp = cur->next;
        cur->next = cur->next->next;
        delete tmp;
        size--;
    }

private:
    int size;
    ListNode* preHead;
};

启发:

1.虚拟头结点在大多数链表相关题目中都能有很好的作用,让我们不用单独去对头结点进行操作。

2.在单向链表中,要想对某一个结点进行删除、插入之类的操作,一定得先移动到它的前一个结点而不能移动到它本身。

24.两两交换链表中的结点

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

思路:

1.交换链表结点实际上就是改变next指向的连线,而在本题中实际改变后的连接稍显混乱,所以不熟练的情况下一定要画图方便理解。

 2.可能在交换的一开始并不知道要临时存储哪些结点,所以在写交换的过程时多思考,发现需要临时存储时就补上。通过画图观察全部过程后可以看出,首先需要存储要交换的两个结点的第一个结点(第二步到第三步),再保存两个结点的第二个结点的之后一个结点(第三步到第四步)。

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        ListNode* preHead = new ListNode(0);
        if(head == nullptr || head->next == nullptr){
            return head;
        }
        preHead->next = head;
        ListNode* cur = preHead;
        while(cur->next != nullptr && cur->next->next != nullptr){
            ListNode* tmp = cur->next;
            ListNode* tmp1 = cur->next->next->next;
            cur->next = cur->next->next;
            cur->next->next = tmp;
            cur->next->next->next = tmp1;

            cur = cur->next->next;
        }
        return preHead->next;
    }
};

启发:

1.交换结点说简单不简单说困难不困难,一定多画图了解整个过程

19.删除链表的倒数第n个结点

题目:给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

思路:

1.在涉及倒数第n个这样的情况时,一般都会采用快慢指针,让快指针先走n步,然后二者再一起走,这样慢指针最后指向的就是倒数第n个元素。但需要注意的是,如果要删除第n个结点,慢指针最后必须指向的是第n-1个结点!

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* preHead = new ListNode(0);
        preHead->next = head;
        ListNode* fast = preHead;
        ListNode* slow = preHead;

        n++;//这里是为了让快指针在最初多移动一位,这样二者同时移动时就会少移动一位,让慢指针最终指向第n-1个结点
        while(n-- && fast!=nullptr){
            fast = fast->next;
        }
        while(fast != nullptr){
            fast = fast->next;
            slow = slow->next;
        }
        ListNode* tmp = slow->next;
        slow ->next = slow->next->next;
        delete tmp;
        return preHead->next;
    }
};

启发:

1.涉及倒数第n个情况,首先考虑快慢指针

面试题02.07 链表相交

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。

图示两个链表在节点 c1 开始相交:

C++快慢指针法

思路: 

1.需要注意要找的是相交的结点,并不是说结点值相同就一定是相交结点。

2.想要的效果肯定是让两个指针分别指向两个链表,并且同时指向相交的结点。为了达到这个目的我们可以求出两条链表的长度,求出二者长度之差,然后让较长链表的结点先走长度的差值,这样两个指针就做到了站在同一起跑线的效果。本题中为了方便,一直让A链表作为较长的链表。

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode* curA = headA;
        ListNode* curB = headB;
        int lenA = 0, lenB = 0;
        while(curA!=NULL){
            curA = curA->next;
            lenA++;
        }
        while(curB!=NULL){
            curB = curB->next;
            lenB++;
        }
        curA = headA;
        curB = headB;
        if(lenB >lenA){
            swap(curA,curB);
            swap(lenA,lenB);
        }
        int gap = lenA - lenB;
        while(gap--){
            curA = curA->next;
        }
        while(curA!=NULL){
            if(curA == curB){
                return curA;
            }
            curA = curA->next;
            curB = curB->next;
        }
        return NULL;
    }
};

C#字典法

思路:

1.另一种思路是,借助哈希表来存储每一个结点及其对应的值,让结点值作为键,让结点作为值。每遇到一个哈希表中未有的键时就将该节点存储进入哈希表,在遇到哈希表中已有的键时,说明当前结点的值在之前已经被记录过,此时再比较该键所对应的值看二者是否是同一节点。不过笔者在写这道题时对哈希表仍不熟练,所以采用的C#字典来完成同样的操作。

public class Solution {
    public ListNode GetIntersectionNode(ListNode headA, ListNode headB) {
        Dictionary<int,ListNode> dict = new Dictionary<int,ListNode>();
        ListNode tmpA = headA;
        ListNode tmpB = headB;
        while(tmpA!=null || tmpB != null){
            if(tmpA!=null){
                if(!dict.ContainsKey(tmpA.val)){
                    dict.Add(tmpA.val,tmpA);
                    tmpA = tmpA.next;
                }
                else{
                    if(dict[tmpA.val] == tmpA){
                        return tmpA;
                    }
                    else{
                        tmpA = tmpA.next;
                    }
                }
            }
            if(tmpB!=null){
                if(!dict.ContainsKey(tmpB.val)){
                    dict.Add(tmpB.val,tmpB);
                    tmpB = tmpB.next;
                }
                else{
                    if(dict[tmpB.val] == tmpB){
                        return tmpB;
                    }
                    else{
                        tmpB = tmpB.next;
                    }
                }
            }
        }
        return null;
    }
}

启发:

1.对于判断两个链表是否相交,如何找到相交的结点非常重要。本题中两个方法分别对应两种思路:一种是让遍历两个链表的指针站在同一起跑线上;一种是存储已遍历过的结点并与当前结点进行比较。

142.环形链表

给定一个链表的头节点  head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

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

不允许修改 链表。

 思路:

1.本题主要解决两个问题:(1)如何判断链表有环?(2)如何找到环入口的结点索引

2.对于问题一,可以用快慢指针的方法。快指针每次走两格,慢指针每次走一格,如果存在环,二者一定会在环内相遇

3.对于问题二,需要通过一定的数学推理。注意我们要求的是x

 我们假设两个指针相遇时,慢指针走了x+y的节点数,快指针则走了x + y + n(y + z)节点数,因为快指针走的结点个数一定是慢指针的两倍,所以可以列出方程2(x+y) = x + y + n(y + z)。经过一系列化简后可以得到x = (n-1)(y + z) + z,其中当n=1时,x = z。这样就代表,当两个指针一个index1从头结点出发每次走一结点,一个index2从快慢指针相遇的结点出发每次走一个结点,二者相遇的地方即为环入口结点。如果取n>1,依然可以用该法,此时只是说fast指针在环内转了n圈后才与slow相遇,而index2在环内多走了n-1圈才和index1相遇,实际效果一致。

3.为什么慢指针一定是走了x+y个结点数呢?慢指针有没有可能在环内先走了m圈后再和快指针相遇?显然是不可能的,当慢指针进入环内时,一定是快指针追慢指针,因为快指针每次比慢指针多走一个结点,所以快指针最后一定会和慢指针相遇,且一定在慢指针走完一圈前与其相遇。如果从物理的角度来说,快指针对于慢指针的相对速度为1,如果慢指针已经走完了一圈,那么快指针一定走了一圈多,超过了慢指针。但是快指针不可能错过慢指针,所以一定在慢指针走完一圈前相遇。

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

启发:

1.本题第一次将数学逻辑推理引入算法题中,一定要学会拆解问题,本题的核心两大问题搞清楚后就可以一个一个进行解决。

2.关于第二个问题的逻辑推理,结合物理运动学的角度进行思考就会发现快慢指针相遇的特点。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值