算法通关村第一关——6个链表经典问题笔记

在本文中我们使用与LeetCode一致的链表结点定义:

class ListNode(object):
    
    def __init__(self,x):
        #_item存放数据元素
        self.val = x
        #_next是下一个结点的标识
        self.next = None

    def __init__(self,val = 0, next = None)
        self.val = val
        self.next = next

解算法题一个重要的方法就是把常用的数据结构都套用一遍看是否可行,一般都可以找到比较适合的方法。

一、两个链表第一个公共子节点

 1.1 解法一:暴力

        遍历A的所有结点,对A的每一个结点都遍历一下B,寻找是否有相同,时间复杂度过高,pass掉

1.2 解法二:哈希or集合

        遍历A,然后用哈希或集合的方式寻找B中是否有相同元素,时间复杂度得到可观改善

1.3 解法三:栈

        A入A栈,B入B栈,两栈同时pop元素,直到不相同

        代码如下:

        

def getIntersectionNode(self,headA,headB):
    s1,s2 = [],[]
    p,q = headA, headB
    while p:
        s1.append(p)
        p = p.next
    while q:
        s2.append(q)
        q = q.next
    ans = None
    i,j = len(s1) - 1, len(s2) - 1
    while i >= 0 and j >= 0 and s1[i] == s2[j]
        ans = s1[i]
        i,j = i - 1, j - 1
    return ans

二、判断链表是否为回文序列

本文中暂时回避将链表元素提取至数组的方法

2.1 解法一:栈

基本思想是两个栈分别从首尾进行比较,实现方法略有不同,尽量高效即可。下面给出一种解法:

  • 遍历一遍得到总长度,同时压栈
  • 再次遍历,但只遍历一半,同时出栈进行比较,实现首尾比较

2.2 解法二:反转链表

基本思想是一边遍历一遍头插法反转链表,然后两个链表同时遍历比较,实现首尾对比

下面给出一种解法:

  • 使用双指针中的快慢指针,fast一次两步,slow一次一步,fast到达表尾时slow刚好处于一半,得到链表半长。
  • 逆序一半
  • 两个链表比较

因为python的列表使用起来非常方便,因此下面的代码借用列表的下标索引快速实现了目的。

def isPalindrome(self,head):
    cur,length = head,0
    result = []
    #入栈
    while cur is not None:    
        length += 1
        result.append(cur.val)
        cur = cur.next
    #定义两个指针,一个从头开始往后,一个从最后往前
    left,right = 0, length - 1
    while left < right
        if result[left] != result[right]:
            return False
        left += 1
        right -= 1
    return True

下面是java的栈解法,较为标准一些:

public boolean isPalindrome(ListNode head){
    ListNode temp = head;
    Stack<Integer> stack = new Stack();
    
    while(temp != null){
        stack.push(temp.val);
        temp = temp.next;
    }

    //一边出栈,一边比较
    while(head!= null){
        if(head.val != stack.pop()){
            return false;
        }
        head = head.next;
    }
    return true;
}

三、合并有序链表

3.1 合并两个有序链表

3.1.1. 解法:传统插入删除操作

  • 建立新链表,每次比较出最小元素插入,一边链表比完后剩余的直接接在后面
  • 将某一个链表中的元素依次插入到另一个链表的对应位置

下面给出插入最小元素的解法:

def mergeTwoLists(self,list1,list2):
    // 新建了一个表头
    phead = ListNode(0)
    p = phead
    while list1 and list2:
        if(list1.val <= list.val):
            p.next = list1
            list1 = list1.next
        else :
            p.next = list2
            list2 = list2.next
        p = p.next
    if list1 == None:
        p.next = list2
    else:
        p.next = list1
    return phead

3.2合并K个链表

        合并k个链表是合并两个链表的延申,外加一个循环处理一下细节即可。

四、双指针专题

4.1 寻找中间结点

前面已经介绍过快慢指针来找到中间节点的问题,现在需要进一步讨论当结点为偶数个和奇数个时的返回情况。假设共有N个结点:

  • 奇数个:设指针共跳m次,则结点共1+2m个,slow指针指向了第1+m个结点

  • 偶数个:设指针共跳m次,则结点共2m个,slow指针指向了第1+m个结点

由此可知,N为奇数时落在第(N+1)/2个,N为偶数时落在第N/2 + 1 个

如果按下标来算,还需要都减去1.

下面给出python代码:

class MiddleNode:
    def middleNode(self,head):
        if(head) is None:
            return None

        slow = head
        fast = head

        while fast and fast.next
            slow = slow.next
            fast = fast.next.next
        return slow

if __name__ == ‘__main__’:
    nums = [1,2,3,4,5,6]
    list = init_list(nums)
    middleNode = MiddleNode()
    node = middleNode.middleNode(list)
    print node.val

4.2 寻找倒数第K个元素

双指针的思想来解题,会比较容易地想到让fast先走到第k+1个结点,随后slow跟着一起走,此时fast和slow二者之间正好间隔k个结点,当fast为空时,slow正好走到倒数第K个结点。

此处需要注意原链表的长可能小于K,需要另外判断一下。

下面给出python代码:

class GetKthFromEnd:
    def getKthFromEnd(self,head:ListNode,k:int) -> ListNode:
        former,latter = head , head
        for _ in range(k):
            if not former: return
            former = former.next
        while(former):
            former = former.next  
            latter = latter.next
        return latter 

   4.3 旋转链表

给你一个链表的头结点head,旋转链表,将链表每个结点向右移动k个位置

4.3.1 解法一:反转链表

把整个链表反转为{5,4,3,2,1},再将前K和后N-K两个部分分别反转,即分别变成{4,5}和{1,2,3}即可。

4.3.1 解法二:快慢指针

找到倒数第K个结点,从这里开始断开重新拼接即可,快指针接到原链表表头。如果不用快慢指针,从正向找第N-K+1个结点就行了.(此处需要通过取模来防止K越界)

class RotateRight:
    def rotateRight(self,head,k):
        if  head is None or head.next is None:
            return head

        num = 0
        basic_head = head
        #统计长度,顺便把原链表尾部的next设为原链表表头
        while head:
            if head.next is None:
                head.next = basic_head
                num += 1
                break
            num += 1
            head = head.next
        
    xx = num - (k%num) #从头往前走xx步,第N-K+1个就是走N-K步
    for i in range(xx):
        if i == (xx - 1):
            flag = basic_head
            basic_head = basic_head.next
            flag.next = None
            break
        basic_head = basic_head.next

    return basic_head

if __name__ == '__main__':
    #提供一个简单的测试样例
    nums = [1,2,3,4,5]
rotateRight = RotateRight()
list = init_list(nums)
node = rotateRight.rotateRight(list,2)

五、删除链表元素专题

 5.1 删除特定结点

给定一个表头结点和一个整数val,删除链表中所有满足Node.val = val的结点,并返回新的头结点。

要删除一个结点,需要知道它的前驱和后继,因此前驱很重要。

此处,我们可以创建一个虚拟结点,这样就不用单独处理头结点的删除问题了

  • 创建虚拟结点,指向头结点
  • 用node.next.val来判断,这样一来得到的直接是被删除结点的前驱。
  • 保存被删除结点待释放
  • 前驱直接指向后继
  • 释放被删除结点

下面的java代码符合上述逻辑:

public ListNode removeElements(ListNode head ,int val){
    ListNode dummyNode = new ListNode(0);
    dummyNode.next = head;
    ListNode cur = dummyhead;
    while(cur.next != null){
        if(cur.next.val == val) {
            cur.next = cur.next.next;
        }
        else {
            cur = cur.next;
        }
    }  
    return dummyNode.next;  
}    

下面的python则是使用了另一种逻辑:

class RomoveElements:
    def removeElements(self,head,val):
        #先把前面连续的被删结点忽略掉
        while head and head.val == val:
            head = head.next
        #如果被删完的话直接返回不用继续删除了
        if head is None:
            return head
        #删除剩下部分中的符合条件结点
        node = head
        while node.next:
            if node.next.val == val:
                node.next = node.next.next
            else:
                node = node.next
            return head

5.2 删除倒数第n个结点

基本思想就是找到倒数第n+1个结点删除结点即可,在怎么找的问题上可以做文章。

  1. 遍历一遍得到总长度,再找到第L-N+1个元素
  2. 全部压栈,再出栈第N个
  3. 双指针

5.3 删除重复元素

给定一个按升序排列的链表,删除所有重复的元素

基本思想是某一个结点与下一个结点值相同时,删除下一个结点即可,遍历完链表后返回头结点即可~

六、再论公共子节点

在问题一中是否有空间复杂度为O(1)的方法呢?

6.1 拼接两个字符串

A:0-1-2-3-4-5
B:a-b-4-5

分别拼接成AB和BA:

AB:0-1-2-3-4-5-a-b-4-5

BA:a-b-4-5-0-1-2-3-4-5

尾部是一样的!而且,只要在原来链表的基础上修改表头和尾部next就可以达到空间复杂度O(1)的目的!

这样就可以同步遍历对比了!一个链表比完之后跳到另一个链表继续比!nice!

下面给出python代码:
 

def getInsertsectionNode3 (self,headA,headB):
    node1,node2 = headA,headB
    while node1 != node2:
        node1 = node1.next if node1 else headB
        node2 = node2.next if node2 else headA

    return node1

一定只能让它俩都只能跳一次,不然死循环出不来了

6.2 差和双指针

先确定一下两个链表它们的长度差,让长的链表指针先走,短的后走,这样就一定会对比到公共节点。

以上就是本期的全部内容啦~

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值