代码随想录算法训练营第二十九天 | Leetcode随机抽题检测

Leetcode随机抽题检测

160 相交链表

未看解答自己编写的青春版

class Solution:
    def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> Optional[ListNode]:
        curA = headA
        curB = headB
        countA = 0
        countB = 0
        while curA :
            countA += 1
            curA = curA.next
        while curB :
            countB += 1
            curB = curB.next
        
        if countA < countB :
            countA,countB = countB,countA
            headA,headB = headB,headA
        diff = countA - countB
        while diff > 0 :
            headA = headA.next
            diff -= 1
        while headA :
            if headA == headB :
                return headB
            else :
                headA = headA.next
                headB = headB.next
        return None
        

重点

过。

题解的代码

206 反转链表

一段用于复制的标题

未看解答自己编写的青春版

class Solution:
    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        pre = None
        cur = head
        while cur :
            temp = cur.next
            cur.next = pre
            pre = cur
            cur = temp
        return pre

重点

过。

题解的代码

日后再次复习重新写

234 回文链表

未看解答自己编写的青春版

class Solution:
    def isPalindrome(self, head: Optional[ListNode]) -> bool:
        dummy_head = ListNode(0,head)
        slow = fast = dummy_head
        # 这里要注意,快慢指针找中间,要加虚拟头才是对的,不加虚拟头不对!
        # 自己举例一下就发现了。
        while fast and fast.next :
            slow = slow.next
            fast = fast.next.next
        cur = slow.next
        slow.next = None
        pre = None
        while cur :
            temp = cur.next
            cur.next = pre
            pre = cur
            cur = temp
        while pre and head :
            if pre.val != head.val :
                return False
            pre = pre.next
            head = head.next
        return True

重点

通过编写这道题的代码,发现了一个很重要的点,如果要用快慢指针法,寻找链表中点,最严格的方式是:要加入虚拟头!

加入虚拟头的代码:

		dummy_head = ListNode(0,head)
        slow = fast = dummy_head
        while fast and fast.next :
            slow = slow.next
            fast = fast.next.next
        cur = slow.next
        slow.next = None # 切断操作

不加入虚拟头的代码:

		slow = fast = head
        while fast and fast.next :
            slow = slow.next
            fast = fast.next.next
        cur = slow.next
        slow.next = None # 切断操作

下面用A代表慢指针,B代表快指针。

比如:1-2-3-4 ,不加入虚拟头时,一开始A,B均在 1 ,移动一步,A在2,B在3,此时还要继续移动,while不退出,继续移动,A在3,B为None,while退出,此时让A.next = None,做切断操作,这明显是错误的!应该是从 2 切断!加入虚拟头后,就是正确的结果。

加入虚拟头,可以让偶数个数的链表切割正确,奇数个数的链表切割结果不变,因为奇数个数的链表,正确切割结果,就是左边要包括中间节点,然后中间节点的next为None。

那么之前有一道题:143 重排链表,为什么卡哥的代码,也是快慢指针找中间,却没有加入虚拟头?
143 重排链表 卡哥的代码:

class Solution:
    def reorderList(self, head: Optional[ListNode]) -> None:
        """
        Do not return anything, modify head in-place instead.
        """
        fast = slow = head

        # find mid point which including (first) mid point into the first half linked list
        while fast and fast.next:
            fast = fast.next.next
            slow = slow.next
        # 下面两句代码别忘了,一个是取右半边,这样取的右半边一定是短的那一方
        # 这样连接也符合题目要求
        right = slow.next # 获取后半边的头
        slow.next = None # 切断!这句话很重要,不然就成环了
        node = None
        while right:
            temp = right.next
            right.next = node
            node = right
            right = temp
        head2 = node
        head1 = head
        while head1 and head2:
            temp1 = head1.next
            temp2 = head2.next
            head1.next = head2
            head2.next = temp1
            head1 = temp1
            head2 = temp2

在这里插入图片描述
因为这道题目的特殊性!可以看出,从2处切割,本题的节点串联逻辑是:1 – 4 – 2 – 3 。从3处切割,本题的节点串联逻辑是:1 – 4 – ( 2 3 ) ,( 2 3 ) 本来就在左子串中,无需改变位置!这是这道题的特殊性!对于偶数个数的链表,中间两个点,切不切割都一样!因为这两个点的顺序不需要颠倒。

综上,利用快慢指针找寻链表中间,就按加入虚拟头的方法写。

题解的代码

日后再次复习重新写

141 环形链表

未看解答自己编写的青春版

class Solution:
    def hasCycle(self, head: Optional[ListNode]) -> bool:
        if head == None or head.next == None :
            return False
        slow = fast = head
        while fast and fast.next : 
            slow = slow.next
            fast = fast.next.next
            # 注意判断的位置,先移动再判断,不然因为初始化都是头结点,会立刻返回True
            if slow == fast :
                return True
        return False

重点

在使用快慢指针时,注意 if 逻辑判断的位置。

注意!注意!警告!警告!上面代码中的提前判断,不是必须的。可以不加。

class Solution:
    def hasCycle(self, head: Optional[ListNode]) -> bool:
        slow = fast = head
        while fast and fast.next : 
            slow = slow.next
            fast = fast.next.next
            # 注意判断的位置,先移动再判断,不然因为初始化都是头结点,会立刻返回True
            if slow == fast :
                return True
        return False

题解的代码

日后再次复习重新写

142 环形链表 II

未看解答自己编写的青春版

class Solution:
    def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
        slow = head
        fast = head
        while fast and fast.next :
            slow = slow.next
            fast = fast.next.next
            if slow == fast :
                cur = head
                while True :
                    if cur == slow :
                        return cur
                    cur = cur.next
                    slow = slow.next
        return None

重点

过,这道题的理论基础,去复习,卡哥的解答。

环形链表 II

题解的代码

日后再次复习重新写

21 合并两个有序链表

未看解答自己编写的青春版

class Solution:
    def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
        newhead = ListNode()
        res = newhead
        while list1 and list2 :
            if list1.val >= list2.val :
                newhead.next = list2
                list2 = list2.next     
            else :
                newhead.next = list1
                list1 = list1.next
            newhead = newhead.next
        if list1 == None and list2 != None :
            newhead.next = list2
        elif list1 != None and list2 == None :
            newhead.next = list1
        return res.next

重点

过。

题解的代码

日后再次复习重新写

2 两数相加

未看解答自己编写的青春版

class Solution:
    def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
        reshead = ListNode()
        cur = reshead
        pre = 0
        while l1 or l2 :
            if l1 == None :
                temp = pre+l2.val
                if temp > 9 :
                    pre = 1
                    temp = temp % 10
                else :
                    pre = 0
                node = ListNode(temp)
                l2 = l2.next
            elif l2 == None :
                temp = pre+l1.val
                if temp > 9 :
                    pre = 1
                    temp = temp % 10
                else :
                    pre = 0
                node = ListNode(temp)
                l1 = l1.next
            else :
                temp = pre+l2.val+l1.val
                if temp > 9 :
                    pre = 1
                    temp = temp % 10
                else :
                    pre = 0
                node = ListNode(temp)
                l1 = l1.next
                l2 = l2.next
            cur.next = node
            cur = cur.next
        if pre == 1 :
            node = ListNode(1)
            cur.next = node
        return reshead.next

重点

过。

题解的代码

日后再次复习重新写

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

未看解答自己编写的青春版

class Solution:
    def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
        dummy_head = ListNode(0,head)
        slow = fast = dummy_head
        while n > 0 :
            fast = fast.next
            n -= 1
        while fast and fast.next :
            fast = fast.next
            slow = slow.next
        slow.next = slow.next.next
        return dummy_head.next

重点

这道题就明确两点:

1、加入虚拟头。

2、模拟特殊情况检验代码是否正确:删除的是最后一个节点,删除的是第一个节点。

上面的代码其实有些冗余了,这道题只要能想到加入虚拟头,上面说的两种特殊情况都能迎刃而解,用一般的示例来考虑就能通过。卡哥的代码中,是先让 fast 移动 n+1 步,我的是先移动 n 步,都可以,不同的对用体现在while判断条件上。

移动 n+1 步 :

class Solution:
    def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
        dummy_head = ListNode(0,head)
        slow = fast = dummy_head
        while n > -1 :
            fast = fast.next
            n -= 1
        while fast :
            fast = fast.next
            slow = slow.next
        slow.next = slow.next.next
        return dummy_head.next

移动 n 步 :

class Solution:
    def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
        dummy_head = ListNode(0,head)
        slow = fast = dummy_head
        while n > 0 :
            fast = fast.next
            n -= 1
        while fast.next :
            fast = fast.next
            slow = slow.next
        slow.next = slow.next.next
        return dummy_head.next

题解的代码

日后再次复习重新写

24 两两交换链表中的节点

未看解答自己编写的青春版

class Solution:
    def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
        if head == None or head.next == None :
            return head
        dummy_head = ListNode(0,head)

        pre = dummy_head
        cur = head
        while cur and cur.next :
            temp1 = cur.next
            temp2 = cur.next.next
            pre.next = temp1
            temp1.next = cur
            cur.next = temp2
            pre = cur
            cur = cur.next
        return dummy_head.next

重点

理清楚while循环中的交换逻辑就好,逻辑弄不清楚,就多搞一个临时变量嘛,两个temp,逻辑不就非常清晰。

这道题为了交换方便,同样也需要用虚拟头。

题解的代码

日后再次复习重新写

25 K 个一组翻转链表

未看解答自己编写的青春版

哈哈哈,独立完成 hard 题 !不过看评论,很多人也做出来了,看来这道题很简单,也就是中等的实际难度。

耗时上也还行,40% 左右。

class Solution:
    def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:

        dummy_head = ListNode(0,head)
        pre = dummy_head
        begin = end = dummy_head.next
        count = 1
        while end :
            if count < k :
                end = end.next
                count += 1
            else :
                temp = end.next
                a,b = self.reverse(begin,end)
                pre.next = b
                pre = a
                a.next = temp
                begin = end = temp
                count = 1
        return dummy_head.next

    def reverse(self,head,end):
        end.next = None
        pre = None
        cur = head
        while cur :
            temp = cur.next
            cur.next = pre
            pre = cur
            cur = temp
        return head,end

重点

评论中的一个解答:

class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        ListNode dummy = new ListNode(0), prev = dummy, curr = head, temp;
        dummy.next = head;
        int length = 0;
        while(head != null) {
            length++;
            head = head.next;
        }
        head = dummy.next;
        for(int i = 0; i < length / k; i++) {
            for(int j = 0; j < k - 1; j++) {
                temp = curr.next;
                curr.next = temp.next;
                temp.next = prev.next;
                prev.next = temp;
            }
            prev = curr;
            curr = prev.next;
        }
        return dummy.next;
    }
}

上面那个评论的方法真厉害,我悟了,第一次接触这种方式来翻转链表的,但是出人意料的很适合这道题!之前做的反转链表的方法,不能直接用到本题上,想使用就要像我那样去写,去谨慎的赋值。这种翻转的思想是:一次交换两个节点,且保持 pre 不变。例如:0–1–2–3–4,pre在0处,不参与翻转,cur在1处。那么第一次操作,是:0–2–1–3–4,此时pre还在0,cur也还在1,继续:0–3–2–1–4,继续:0–4–3–2–1,它是每次都将后面的一个值,插入到pre的后面,pre不移动,cur也不移动,但是cur的相对位置,会随着链表的更改而移动。

想清楚,节点的移动逻辑,再谨慎地编写每次循环中的逻辑就可以了。

class Solution:
    def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:

        dummy_head = ListNode(0,head)
        pre = dummy_head
        cur = dummy_head.next
        aa = dummy_head.next
        count = 0
        while aa :
            count += 1
            aa = aa.next
        for i in range(count//k):
            for j in range(k-1):
                # 这里的四个赋值,顺序非常有讲究!
                temp = cur.next
                cur.next = temp.next
                temp.next = pre.next
                pre.next = temp
                  
            pre = cur
            cur = cur.next
        return dummy_head.next

题解的代码

日后再次复习重新写

138 复制带随机指针的链表

未看解答自己编写的青春版

思想很朴素,每次都从头搜索,random节点,所以耗时也只打败了6%。去学习一下评论区。

class Solution:
    def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]':
        reshead = Node(0)
        copycur = reshead
        cur = head
        while cur :
            node = Node(cur.val)
            copycur.next = node
            copycur = copycur.next
            cur = cur.next

        copycur = reshead.next
        cur = head
        while cur :
            if cur.random == None :
                copycur.random == None  
            else :
                temp_org = head
                temp_copy = reshead.next

                while temp_org != cur.random :
                    temp_org = temp_org.next
                    temp_copy = temp_copy.next
                copycur.random = temp_copy
            cur = cur.next
            copycur = copycur.next
        return reshead.next

重点

评论中的一个,递归的解法:很有意思。当然这种方法相当于记录了该链表的所有节点,空间复杂度是O(n),虽然是递归,但是这道题的时间复杂度也是O(n)。

class Solution:
    def copyRandomList(self, head):
        def copyNode(node, res):
            if not node: return None
            if node in res: return res[node]
            copy = Node(node.val, None, None)
            res[node] = copy
            copy.next = copyNode(node.next, res)
            copy.random = copyNode(node.random, res)
            return copy

        return copyNode(head, {})

另一种解法,哈希,利用字典去记录,原节点和新节点的映射关系。使用hash存储原结点和克隆结点的映射关系,通过映射关系处理克隆结点的random指针。

时间复杂度和空间复杂度,均为O(n)

class Solution {
    public Node copyRandomList(Node head) {
        if(head == null){
            return head;
        }
        // map方法,空间复杂度O(n)
        Node node = head;
        // 使用hash表存储旧结点和新结点的映射
        Map<Node,Node> map = new HashMap<>();
        while(node != null){
            Node clone = new Node(node.val,null,null);
            map.put(node,clone);
            node = node.next;
        }
        node = head;
        while(node != null){
            map.get(node).next = map.get(node.next);
            map.get(node).random = map.get(node.random);
            node = node.next;
        }
        return map.get(head);
    }
}

通过对上面两种方法的学习,发现了这道题的本质,就是在利用next建立新链表的时候,利用一个map,保存好原节点和新节点的对应关系就好了!本质上是考哈希!

另一种非常牛的方法,随机指针复制+拆分

原地处理,将克隆结点放在原结点后面,在原链表上处理克隆结点的random指针,最后分离两个链表,空间复杂度O(1)。

class Solution {
    public Node copyRandomList(Node head) {
        if(head == null){
            return head;
        }
        // 空间复杂度O(1),将克隆结点放在原结点后面
        Node node = head;
        // 1->2->3  ==>  1->1'->2->2'->3->3'
        while(node != null){
            Node clone = new Node(node.val,node.next,null);
            Node temp = node.next;
            node.next = clone;
            node = temp;
        }
        // 处理random指针
        node = head;
        while(node != null){
            // !!
            node.next.random = node.random == null ? null : node.random.next;
            node = node.next.next;
        }
        // 还原原始链表,即分离原链表和克隆链表
        node = head;
        Node cloneHead = head.next;
        while(node.next != null){
            Node temp = node.next;
            node.next = node.next.next;
            node = temp;
        }
        return cloneHead;
    }
}

一篇有助于理解的题解链接:

清楚的题解

这题太牛了,一定要着重复习后面,这种思想第一次接触。

题解的代码

日后再次复习重新写

哈希方法复写:

class Solution:
    def copyRandomList(self, head):
        reshead = Node(0)
        copycur = reshead
        cur = head
        table = {}
        while cur :
            node = Node(cur.val)
            copycur.next = node
            copycur = copycur.next
            table[cur] = copycur
            cur = cur.next

        copycur = reshead.next
        cur = head
        while cur :
            if cur.random == None :
                copycur.random == None  
            else :
                copycur.random = table[cur.random]
            cur = cur.next
            copycur = copycur.next
            
        return reshead.next

148 排序链表

未看解答自己编写的青春版

先遍历得到数组,再排序,用一个字典存储:数组排序后的下标(key)和链表中的节点(value)的映射,然后按照key,从小到大去索引原节点,来构造新链表。这是时间复杂度为O(n logn),空间复杂度为O(n)的做法。

速度很快,打败95%,但是空间上只打败了6%

class Solution:
    def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        table = {}
        nums = []
        cur = head
        while cur :
            # 处理值相同的情况
            if cur.val in table :
                # 这里要直接 append, 不能采用赋值操作:table[cur.val] = table[cur.val].append(cur)
                # 因为 table[cur.val].append(cur) 的返回值是None
                # 下面的 pop 操作同理
                table[cur.val].append(cur)
            else :
                table[cur.val] = [cur]
            nums.append(cur.val)
            cur = cur.next
        nums.sort()
        reshead = ListNode()
        cur = reshead
        
        for i in nums:
            # 注意这里,要从最末尾开始取,因为pop就是丢掉末尾的
            node = table[i][-1]  
            table[i].pop()
            cur.next = node
            cur = cur.next
        cur.next = None
        return reshead.next

要使用O(1)的空间复杂度,必然是对链表直接进行排序操作,对链表的排序还从来没写过,采用哪种方式好也不清楚,冒泡肯定不行,是O(n^2),看题解说是,归并排序,归并排序是最适合链表这种数据结构的排序方式。快速排序也可以。

重点,这道题太重要了太重要了,一定要多次复习。

归并排序,是对链表排序最好的方法

在这里插入图片描述
伪代码:

current = dummy.next;
tail = dummy;
for (step = 1; step < length; step *= 2) {
	while (current) {
		// left->@->@->@->@->@->@->null
		left = current;

		// left->@->@->null   right->@->@->@->@->null
		right = cut(current, step); // 将 current 切掉前 step 个头切下来。

		// left->@->@->null   right->@->@->null   current->@->@->null
		current = cut(right, step); // 将 right 切掉前 step 个头切下来。
		
		// dummy.next -> @->@->@->@->null,最后一个节点是 tail,始终记录
		//                        ^
		//                        tail
		tail.next = merge(left, right);
		while (tail->next) tail = tail->next; // 保持 tail 为尾部
	}
}

正式代码:好像是C ?

class Solution {
public:
    ListNode* sortList(ListNode* head) {
        ListNode dummyHead(0);
        dummyHead.next = head;
        auto p = head;
        int length = 0;
        while (p) {
            ++length;
            p = p->next;
        }
        
        for (int size = 1; size < length; size <<= 1) {
            auto cur = dummyHead.next;
            auto tail = &dummyHead;
            
            while (cur) {
                auto left = cur;
                auto right = cut(left, size); // left->@->@ right->@->@->@...
                cur = cut(right, size); // left->@->@ right->@->@  cur->@->...
                
                tail->next = merge(left, right);
                while (tail->next) {
                    tail = tail->next;
                }
            }
        }
        return dummyHead.next;
    }
    
    ListNode* cut(ListNode* head, int n) {
        auto p = head;
        while (--n && p) {
            p = p->next;
        }
        
        if (!p) return nullptr;
        
        auto next = p->next;
        p->next = nullptr;
        return next;
    }
    
    ListNode* merge(ListNode* l1, ListNode* l2) {
        ListNode dummyHead(0);
        auto p = &dummyHead;
        while (l1 && l2) {
            if (l1->val < l2->val) {
                p->next = l1;
                p = l1;
                l1 = l1->next;       
            } else {
                p->next = l2;
                p = l2;
                l2 = l2->next;
            }
        }
        p->next = l1 ? l1 : l2;
        return dummyHead.next;
    }
};

一定要好好学习归并排序,这是可以作为模板代码来学习的!本质上就是两个操作,merge 和 cut 。从网上搜到的很多归并排序都是用递归写的,不过本题使用循环来写,觉得这种写法很值得学习!

归并排序的递归和循环写法,都值得学习!

快速排序版本:是交换节点的,并非只交换数值。(暂时没看)

class Solution {
public ListNode sortList(ListNode head) {
        if(head==null||head.next==null) return head;
        // 没有条件,创造条件。自己添加头节点,最后返回时去掉即可。
        ListNode newHead=new ListNode(-1);
        newHead.next=head;
        return quickSort(newHead,null);
    }
    // 带头结点的链表快速排序
    private ListNode quickSort(ListNode head,ListNode end){
        if (head==end||head.next==end||head.next.next==end) return head;
        // 将小于划分点的值存储在临时链表中
        ListNode tmpHead=new ListNode(-1);
        // partition为划分点,p为链表指针,tp为临时链表指针
        ListNode partition=head.next,p=partition,tp=tmpHead;
        // 将小于划分点的结点放到临时链表中
        while (p.next!=end){
            if (p.next.val<partition.val){
                tp.next=p.next;
                tp=tp.next;
                p.next=p.next.next;
            }else {
                p=p.next;
            }
        }
        // 合并临时链表和原链表,将原链表接到临时链表后面即可
        tp.next=head.next;
        // 将临时链表插回原链表,注意是插回!(不做这一步在对右半部分处理时就断链了)
        head.next=tmpHead.next;
        quickSort(head,partition);
        quickSort(partition,end);
        // 题目要求不带头节点,返回结果时去除
        return head.next;
    }
}

题解的代码

日后再次复习重新写

自己复写的归并排序:好好理解!

class Solution:
    def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        dummy_head = ListNode(0,head)
        cur = head
        count = 0
        while cur :
            count += 1
            cur = cur.next
        step = 1
        while step < count :
            # 这里cur从头结点head开始也是必须的,而不能从虚拟头开始
            cur = dummy_head.next
            pre = dummy_head
            while cur :
                left = cur
                right = self.cut(left,step)

                cur = self.cut(right,step)
                pre.next = self.merge(left,right)
                while pre.next :
                    pre = pre.next
            step = step << 1
        return dummy_head.next
        

    def merge(self,head1,head2):
        dummy_head = ListNode()
        cur = dummy_head
        while head1 and head2 :
            if head1.val >= head2.val :
                cur.next = head2
                head2 = head2.next
                cur = cur.next
            else :
                cur.next = head1
                head1 = head1.next
                cur = cur.next
        if head1 == None :
            cur.next = head2
        else :
            cur.next = head1
        return dummy_head.next


    # 注意,cut操作是,返回切断后,后半部分的链表头
    def cut(self,head,n):
        cur = head
        # 注意这里,一定是n>1 ,因为left是从头结点head开始的
        # 那么如果step是1的话,指针应该不移动,这样才能仅cut掉当前节点
        while n > 1 and cur != None :
            cur = cur.next
            n -= 1
        if cur == None :
            return None
        res = cur.next
        # cut 操作要在最后结尾处截断,赋值为 None
        cur.next = None
        return res


23 合并 K 个升序链表

未看解答自己编写的青春版

有了上一题的铺垫,这道题就显得较为简单了,但是效率高不高就不清楚了。倒序归并排序,执行 n-1 次,因为是每次合并两个,然后 pop 出这两个,然后将结果 append 进去。

class Solution:
    def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
        n = len(lists) 
        if lists == []:
            return None
        temp = lists[0]
        while len(lists) > 1 :
            head1 = lists[-1]
            head2 = lists[-2]
            temp = self.merge(head1,head2)
            lists.pop()
            lists.pop()
            lists.append(temp)
        return temp





    def merge(self,head1,head2):
        dummy_head = ListNode()
        cur = dummy_head
        while head1 and head2 :
            if head1.val >= head2.val :
                cur.next = head2
                head2 = head2.next
                cur = cur.next
            else :
                cur.next = head1
                head1 = head1.next
                cur = cur.next
        if head1 == None :
            cur.next = head2
        else :
            cur.next = head1
        return dummy_head.next

重点

这题的评论区里,一堆妖魔鬼怪的方法,作为初学者的我,就找一个最朴素的思路吧,分治法。

可以学习一下,直接没怎么接触过?

其实就是递归,只不过之前的递归,在获得 L1 和 L2 处,要收获结果了,都是一个类似于:加和,append的操作,本题变成了一个两个链表的merge函数。

class Solution:
    def mergeKLists(self, lists: List[ListNode]) -> ListNode:

        n = len(lists)

        def merge(left, right):
            if left > right:
                return
            if left == right:
                return lists[left]
            mid = (left + right) // 2
            l1 = merge(left, mid)
            l2 = merge(mid + 1, right)
            return mergeTwoLists(l1, l2)

        def mergeTwoLists(l1, l2):
            if not l1 or not l2:
                return l1 or l2
            if l1.val < l2.val:
                l1.next = mergeTwoLists(l1.next, l2)
                return l1
            else:
                l2.next = mergeTwoLists(l1, l2.next)
                return l2

        return merge(0, n - 1)

题解的代码

日后再次复习重新写

分治法复写:将原本代码中,归并两个有序链表的 mergeTwoLists 函数的递归写法,改为了一般的循环写法,这样看上去就更好理解一些了,本质上就是递归!

class Solution:
    def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
      
        n = len(lists)

        def merge(left, right):
            if left > right:
                return
            if left == right:
                return lists[left]
            mid = (left + right) // 2
            l1 = merge(left, mid)
            l2 = merge(mid + 1, right)
            return self.mergeTwoLists(l1, l2)

        return merge(0, n - 1)



    def mergeTwoLists(self,head1,head2):
        dummy_head = ListNode()
        cur = dummy_head
        while head1 and head2 :
            if head1.val >= head2.val :
                cur.next = head2
                head2 = head2.next
                cur = cur.next
            else :
                cur.next = head1
                head1 = head1.next
                cur = cur.next
        if head1 == None :
            cur.next = head2
        else :
            cur.next = head1
        return dummy_head.next

146 LRU 缓存

未看解答自己编写的青春版

没见过这种类型的题,也不知道应该用什么数据结构。

重点

首先要明确本题的两个要点。

1、LRU 的功能可以使用双向链表实现,访问到的节点移动到头部,超出容量的从尾部删除。

2、要实现O(1)得使用HaspMap,里面储存 key 与 链表节点即可,这样可以快速定位节点,然后删除它,将它移动到链表头部。

这道双向链表的题目,真的学习了,后面一定要多复习多重写这道题!

题解的代码

class ListNode:
    def __init__(self, key=None, value=None):
        self.key = key
        self.value = value
        self.prev = None
        self.next = None


class LRUCache:
    def __init__(self, capacity: int):
        self.capacity = capacity
        self.hashmap = {}
        # 新建两个节点 head 和 tail
        self.head = ListNode()
        self.tail = ListNode()
        # 初始化链表为 head <-> tail
        self.head.next = self.tail
        self.tail.prev = self.head

    # 因为get与put操作都可能需要将双向链表中的某个节点移到末尾,所以定义一个方法
    def move_node_to_tail(self, key):
            # 先将哈希表key指向的节点拎出来,为了简洁起名node
            #      hashmap[key]                               hashmap[key]
            #           |                                          |
            #           V              -->                         V
            # prev <-> node <-> next         pre <-> next   ...   node
            node = self.hashmap[key]
            node.prev.next = node.next
            node.next.prev = node.prev
            # 之后将node插入到尾节点前
            #                 hashmap[key]                 hashmap[key]
            #                      |                            |
            #                      V        -->                 V
            # prev <-> tail  ...  node                prev <-> node <-> tail
            node.prev = self.tail.prev
            node.next = self.tail
            self.tail.prev.next = node
            self.tail.prev = node

    def get(self, key: int) -> int:
        if key in self.hashmap:
            # 如果已经在链表中了久把它移到末尾(变成最新访问的)
            self.move_node_to_tail(key)
        res = self.hashmap.get(key, -1)
        if res == -1:
            return res
        else:
            return res.value

    def put(self, key: int, value: int) -> None:
        if key in self.hashmap:
            # 如果key本身已经在哈希表中了就不需要在链表中加入新的节点
            # 但是需要更新字典该值对应节点的value
            self.hashmap[key].value = value
            # 之后将该节点移到末尾
            self.move_node_to_tail(key)
        else:
            if len(self.hashmap) == self.capacity:
                # 去掉哈希表对应项
                self.hashmap.pop(self.head.next.key)
                # 去掉最久没有被访问过的节点,即头节点之后的节点
                self.head.next = self.head.next.next
                self.head.next.prev = self.head
            # 如果不在的话就插入到尾节点前
            new = ListNode(key, value)
            self.hashmap[key] = new
            new.prev = self.tail.prev
            new.next = self.tail
            self.tail.prev.next = new
            self.tail.prev = new

日后再次复习重新写

94 二叉树的中序遍历

未看解答自己编写的青春版

class Solution:
    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        if root == None :
            return []
        left = self.inorderTraversal(root.left)
        middle = root.val
        right = self.inorderTraversal(root.right)

        return left+[middle]+right
        

重点

过。本次刷题旨在把题做出来,不考虑,递归法怎么写,迭代法怎么写,其他方法有没有这种事情了。

题解的代码

日后再次复习重新写

104 二叉树的最大深度

未看解答自己编写的青春版

class Solution:
    def maxDepth(self, root: Optional[TreeNode]) -> int:
        if root == None :
            return 0
        left = self.maxDepth(root.left)
        right = self.maxDepth(root.right)
        return 1 + max(left,right)

重点

最大深度 = 根节点的最大高度,直接用高度的定义去递归,简单!

题解的代码

日后再次复习重新写

226 翻转二叉树

未看解答自己编写的青春版

正确可以AC的代码:

class Solution:
    def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
        if root == None :
            return None
        left = self.invertTree(root.left)
        right = self.invertTree(root.right)
        root.left , root.right = right,left
        return root

要引以为戒的错误代码:错误原因,在未完全递归完成之前,就改变了当前正在递归节点的左右子树。

class Solution:
    def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
        if root == None :
            return None
        root.left = self.invertTree(root.right)
        # 上一行代码的赋值操作,导致改变了root.left,下面的递归就不对了!
        root.right = self.invertTree(root.left)
        return root

重点

题解的代码

日后再次复习重新写

101 对称二叉树

未看解答自己编写的青春版

这道题没法在原函数上进行递归判断了,必须新建一个函数,因为对称的判断需要左右两棵子树的头结点。

class Solution:   
    def isSymmetric(self, root: Optional[TreeNode]) -> bool:
        if root == None :
            return True
        return self.judge_symmetric(root.left,root.right)

    def judge_symmetric(self,left,right):
        if left == None and right == None :
            return True
        elif left == None and right != None :
            return False
        elif left != None and right == None :
            return False
        else :
            flag1 = self.judge_symmetric(left.left,right.right)
            flag2 = self.judge_symmetric(left.right,right.left)
            if left.val == right.val :
                flag3 = True
            else :
                flag3 = False
            return flag1 and flag2 and flag3

怎么感觉我这次写的这个代码,有点复杂呢。

重点

之前的代码也差不多,可以通过更改顺序,稍微优化一下。

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:  
    def isSymmetric(self, root: Optional[TreeNode]) -> bool:
        if root == None :
            return True
        return self.digui(root.left,root.right)
        
    def digui(self,p,q):
        if p == None and q == None :
            return True
        elif p == None and q != None :
            return False
        elif p != None and q == None :
            return False
        else :
            if p.val != q.val :
                return False
            else :
                left = self.digui(p.left,q.right)
                if left :
                    right = self.digui(p.right,q.left)
                return left and right
        

题解的代码

日后再次复习重新写

543 二叉树的直径

未看解答自己编写的青春版

嘿嘿,无敌。虽然这道题是简单题,但是我觉得这道题如何保存最长的路径,以及怎么处理每个节点的返回值,还蛮需要考虑清楚的。

class Solution:    
    def diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int:
        if root == None :
            return 0
        self.res = 0
        self.track_road(root)
        return self.res

    def track_road(self,root):
        if root == None :
            return 0
        left = self.track_road(root.left)
        right = self.track_road(root.right)
        self.res = max(self.res,left+right)
        return 1 + max(left,right)

重点

我自认为重点有两个:

1、最长路径不一定经过根节点,比如根节点的左子树很深,而且是完全二叉树,而根节点的右子树只有一个节点,那么最长路径一定出现在左子树中。

2、本题要处理的值,和节点的返回值不一致,不是像之前做过的题目,所求结果就是根节点的返回值!在当前节点,最长路径是左右子树加起来,但是如果要返回到上一层,只能选一个max的返回!

题解的代码

日后再次复习重新写

102 二叉树的层序遍历

未看解答自己编写的青春版

from collections import deque
class Solution:
    def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
        if root == None :
            return []
        dq = deque()
        dq.append(root)
        res = []
        while dq :
            size = len(dq)
            level = []
            for i in range(size):
                node = dq.popleft()
                level.append(node.val)
                if node.left :
                    dq.append(node.left)
                if node.right :
                    dq.append(node.right)
            res.append(level)
        return res

重点

层序遍历,模板题。

题解的代码

日后再次复习重新写

108 将有序数组转换为二叉搜索树

未看解答自己编写的青春版

递归构造就行了。

class Solution:
    def sortedArrayToBST(self, nums: List[int]) -> Optional[TreeNode]:
        if nums == [] :
            return None
        n = len(nums)
        mid = n // 2
        node = TreeNode(nums[mid])
        node.left = self.sortedArrayToBST(nums[0:mid])
        node.right = self.sortedArrayToBST(nums[mid+1:])
        return node

重点

题解的代码

日后再次复习重新写

98 验证二叉搜索树

未看解答自己编写的青春版

中序遍历:左中右,所有的中间节点处理逻辑都要放在中间。

class Solution:
    def __init__(self):
        self.pre = None
    def isValidBST(self, root: Optional[TreeNode]) -> bool:
        if root == None :
            return True
        left = self.isValidBST(root.left)
        # 中序遍历:左中右,所有的中间节点处理逻辑都要放在中间
        if self.pre :
            if self.pre.val >= root.val :
                return False
        # 这句话一定要放在中间
        self.pre = root
        right = self.isValidBST(root.right)       
        return left and right

赋值语句 ( self.pre = root ) ,位置错误,导致的错误代码:

class Solution:
    def __init__(self):
        self.pre = None
    def isValidBST(self, root: Optional[TreeNode]) -> bool:
        if root == None :
            return True
        left = self.isValidBST(root.left)
        # 中序遍历:左中右,所有的中间节点处理逻辑都要放在中间
        if self.pre :
            if self.pre.val >= root.val :
                return False
        right = self.isValidBST(root.right)    
        self.pre = root
        return left and right

中序遍历迭代法:

class Solution:
    def isValidBST(self, root: Optional[TreeNode]) -> bool:
        stack = []
        cur = root
        pre = None
        while stack or cur :
            if cur :
                stack.append(cur)
                cur = cur.left
            else :
                node = stack.pop()
                if pre :
                    if pre.val >= node.val :
                        return False
                pre = node
                cur = node.right
        return True

上面从stack里 pop 出来的值,可以直接用 cur 承接,更顺眼一些。

class Solution:
    def isValidBST(self, root: Optional[TreeNode]) -> bool:
        stack = []
        cur = root
        pre = None
        while stack or cur :
            if cur :
                stack.append(cur)
                cur = cur.left
            else :
                cur = stack.pop()
                if pre :
                    if pre.val >= cur.val :
                        return False
                pre = cur
                cur = cur.right
        return True

重点

题解的代码

日后再次复习重新写

230 二叉搜索树中第K小的元素

未看解答自己编写的青春版

中序遍历的迭代法模板,直接秒了,但是时间上只打败了7% ?

class Solution:
    def kthSmallest(self, root: Optional[TreeNode], k: int) -> int:
        count = 0
        stack = []
        cur = root
        while stack or cur :
            if cur :
                stack.append(cur)
                cur = cur.left
            else :
                cur = stack.pop()
                count += 1
                if count == k:
                    return cur.val
                cur = cur.right
        return 0

重点

从网上看了一些评论,也基本上和我的方法一致。

另一种思路:通过计算节点个数来找寻第K个数,查找左子树节点个数为 leftN , 如果 K<=leftN ,则所查找节点在左子树上,若 K=leftN+1 , 则所查找节点为根节点,若 K>leftN+1 , 则所查找节点在右子树上, 按照同样方法查找右子树第 K-leftN 个节点。

但是在时间上还是打败 7% , 其他解法怎么这么快的?

class Solution:
    def kthSmallest(self, root: Optional[TreeNode], k: int) -> int:
        
        left = self.count(root.left)
        if left + 1 == k:
            return root.val
        elif left >= k :
            return self.kthSmallest(root.left,k)
        else :
            return self.kthSmallest(root.right,k-left-1)

    def count(self,root):
        if root == None :
            return 0
        return self.count(root.left)+self.count(root.right)+1

题解的代码

力扣的示例代码:

class Solution:
    def kthSmallest(self, root: Optional[TreeNode], k: int) -> int:      
        res = []
        def dfs(node):
            if not node:
                return
            if len(res)>=k:
                return
            dfs(node.left)
            res.append(node.val)
            dfs(node.right)
        dfs(root)
        return res[k-1]

原来就是递归,储存所有遍历过的节点,当结果列表长度大于等于 k 时,返回。

看起来因为使用的是递归,所以应该是比我的中序遍历迭代法要少遍历一些节点。不过这个代码,有时候90%,有时候也是7%,不纠结这道题的耗时统计了!

日后再次复习重新写

199 二叉树的右视图

未看解答自己编写的青春版

又是层序遍历模板题。

from collections import deque
class Solution:
    def rightSideView(self, root: Optional[TreeNode]) -> List[int]:
        if root == None :
            return []
        dq = deque()
        dq.append(root)
        res = []
        while dq :
            size = len(dq)
            for i in range(size):
                node = dq.popleft()
                if node.left :
                    dq.append(node.left)
                if node.right:
                    dq.append(node.right)
            res.append(node.val)
        return res

重点

题解的代码

日后再次复习重新写

一段用于复制的标题

未看解答自己编写的青春版

一开始总想着用递归做,但是发现,在递归中,无法时刻保存当前需要赋值的节点,因为本题要求不能返回任意值,也就是要操作原节点,所以递归函数返回值,赋值,这些操作都是无效的,所以应该用前序遍历的迭代法。

class Solution:
    def flatten(self, root: Optional[TreeNode]) -> None:
        """
        Do not return anything, modify root in-place instead.
        """
        if root == None :
            return None
        stack = [root]
        cur = root
        while stack :
            node = stack.pop()
            if node == root :
                pass
            else :
                cur.right = node
                cur.left = None
                cur = node
            if node.right :
                stack.append(node.right)
            if node.left :
                stack.append(node.left)

但是上述是:额外空间复杂度为O(n),因为额外申请了一个堆栈。

重点

额外空间复杂度为O(1)的思想!这道题我被题目骗了,题目说顺序应该是前序遍历,我就想着递归必须用前序遍历,但是前序遍历的问题是:会提前修改掉后面要进入递归的值。那么,链表,从上到下连接可以,我从下到上,反向连接,也可以啊!而这一点,利用递归的回溯特性,可以很自然地做到。假如 last 承载了下一层递归的返回值,而本层递归的root为last的上一个,只需要赋值:root.right=last,就可以了!

所以后序遍历!后序遍历,也就不会有,还未进入递归,值就被修改的问题!这题的思路太妙了!

class Solution:
    def __init__(self):
        self.last = None
    def flatten(self, root: Optional[TreeNode]) -> None:
        """
        Do not return anything, modify root in-place instead.
        """
        if root == None :
            return None
        self.flatten(root.right)
        self.flatten(root.left)
        root.right = self.last
        root.left = None
        self.last = root

思路很妙,可以多写多复习。

题解的代码

日后再次复习重新写

105 从前序与中序遍历序列构造二叉树

未看解答自己编写的青春版

递归,数组切片方法:

class Solution:
    def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
        if preorder == []:
            return None
        middle = preorder[0]
        index = inorder.index(middle)
        
        node = TreeNode(middle)
        node.left = self.buildTree(preorder[1:index+1],inorder[:index])
        node.right = self.buildTree(preorder[index+1:],inorder[index+1:])
        return node

厉害!第一次自己尝试,写出了传入左右索引版的代码,在空间占用上,要比上面的代码低不少。

class Solution:
    def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:

        def build(pleft,pright,ileft,iright):
            # 左闭右开,不包括right,所以才能有等于号
            if pleft >= pright :
                return None
            middle = preorder[pleft]
            index = inorder.index(middle)
            node = TreeNode(middle)
            # 举例: ileft = 3 , middle = 4 , 意味着左子树只有一个节点
            # 那么对于前序数组,起始位置 pleft+1 , 要想让左子树有一个点,区间应该为
            # [pleft+1,pleft+2) 注意循环不变量,左闭右开
            node.left = build(pleft+1,pleft+1+index-ileft,ileft,index)
            node.right = build(pleft+1+index-ileft,pright,index+1,iright)
      
            return node

        n = len(preorder)
        return build(0,n,0,n)

重点

过。可以体会下,传入左右区间的参数的版本,怎样能把每个区间都写对。

题解的代码

日后再次复习重新写

437 路径总和 III

未看解答自己编写的青春版

没做出来。

重点

主要是两种方法:双重递归 ; 前缀和。

力扣官方题解

没来得及体会,日后再看,重点学习这道题。

题解的代码

双重递归:

class Solution:
    def pathSum(self, root: TreeNode, targetSum: int) -> int:
        def rootSum(root, targetSum):
            if root is None:
                return 0

            ret = 0
            if root.val == targetSum:
                ret += 1

            ret += rootSum(root.left, targetSum - root.val)
            ret += rootSum(root.right, targetSum - root.val)
            return ret
        
        if root is None:
            return 0
            
        ret = rootSum(root, targetSum)
        ret += self.pathSum(root.left, targetSum)
        ret += self.pathSum(root.right, targetSum)
        return ret

前缀和:

class Solution:
    def pathSum(self, root: TreeNode, targetSum: int) -> int:
        prefix = collections.defaultdict(int)
        prefix[0] = 1

        def dfs(root, curr):
            if not root:
                return 0
            
            ret = 0
            curr += root.val
            ret += prefix[curr - targetSum]
            prefix[curr] += 1
            ret += dfs(root.left, curr)
            ret += dfs(root.right, curr)
            prefix[curr] -= 1

            return ret

        return dfs(root, 0)

日后再次复习重新写

236 二叉树的最近公共祖先

未看解答自己编写的青春版

搞懂,左右公共祖先,怎么从后序遍历中,层层回溯到根节点的。

class Solution:
    def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
        self.res = None
        self.digui(root,p,q)
        return self.res

    def digui(self,root,p,q):
        if root == None :
            return False
        if root.val == p.val or root.val == q.val :
            self.res = root
            return True               
        left = self.digui(root.left,p,q)
        right = self.digui(root.right,p,q)
        if left and right :
            self.res = root
        return left or right

重点

参考卡哥的解答。

二叉树的最近公共祖先

题目拓展:如果树是一个二叉搜索树呢?前序遍历树中节点的值就可以了,当节点值第一次出现在在区间
[ p.val , q.val ] 时,这个节点就是最近公共祖先,可以用反证法证明,再走一步就不符合条件了。

题解的代码

日后再次复习重新写

124 二叉树中的最大路径和

未看解答自己编写的青春版

小小 hard 。

本题只需要考虑清楚:对于在当前节点收获结果的逻辑:左和右,分别都有两个状态,取或不取,一共四种情况就好了;递归函数在返回时,同前面做过的一道题,返回的时候,没有左右都考虑的情况。

class Solution:
    def maxPathSum(self, root: Optional[TreeNode]) -> int:
        self.res = -inf
        self.digui(root)
        return self.res

    def digui(self,root):
        if root == None :
            return 0
        if root.left == None and root.right == None :
            self.res = max(self.res,root.val)
            return root.val
        left = self.digui(root.left)
        right = self.digui(root.right)
        # 只要想明白这里,左和右,分别都有两个状态,取或不取,一共四种情况就好了
        self.res = max(self.res,left+right+root.val,root.val,right+root.val,left+root.val)
        # 同前面做过的一道题,返回的时候,没有左右都考虑的情况
        return max(left,right,0)+root.val

重点

题解的代码

日后再次复习重新写

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值