Leetcode第二期Task01 链表 707. 设计链表 206.反转链表 203.移除链表 328.奇偶链表 234.回文链表 21.合并两个有序链表 019.删除链表的倒数第 N 个结点


一、链表基础知识

参考链接:链表基础知识
以下内容参考这该链接。

1.1链表定义

链表(Linked List):一种线性表数据结构。它使用一组任意的存储单元(可以是连续的,也可以是不连续的),来存储一组具有相同类型的数据。
可分为单向链表、双向链表、循环链表等等。

1.2链表的基本结构和操作

链表是由节点通过next链接而构成的,所以先来定义一个简单的链节点类,即 ListNode 类。ListNode 类使用成员变量 val表示数据元素的值,使用指针变量 next 表示后继指针。

# 链节点类
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

# 链表类
class LinkedList:
    def __init__(self):
        self.head = None

1.2.1 建立一个线性链表

  • 从所给线性表的第 1 个数据元素开始依次获取表中的数据元素。
  • 每获取一个数据元素,就为该数据元素生成一个新节点,将新节点插入到链表的尾部。
  • 插入完毕之后返回第 1 个链节点的地址。

建立一个线性链表的时间复杂度为 ,n 为线性表长度。

# 根据 data 初始化一个新链表
def create(self, data):
    self.head = ListNode(0)		#创建节点
    cur = self.head
    for i in range(len(data)):
        node = ListNode(data[i])	#创建节点
        cur.next = node
        cur = cur.next

1.2.2 求线性链表的长度

  • 让指针变量 cur 指向链表的第 1 个链节点。
  • 然后顺着链节点的 next 指针遍历链表,指针变量 cur 每指向一个链节点,计数器就做一次计数。
  • 等 cur 指向为空时结束遍历,此时计数器的数值就是链表的长度,将其返回即可。

求线性链表的长度操作的问题规模是链表的链节点数 n,基本操作是 cur 指针的移动,操作的次数为 n,因此算法的时间复杂度为 。

# 获取链表长度
def length(self):
    count = 0
    cur = self.head
    while cur:
        count += 1
        cur = cur.next 
    return count

其实也可以使用一个self.size变量去储存链表的长度。该操作就变成O(1)了。

1.2.3 查找元素

在链表中查找值为 val 的位置:链表不能像数组那样进行随机访问,只能从头节点 head 开始,沿着链表一个一个节点逐一进行查找。如果查找成功,返回被查找节点的地址。否则返回 None。
查找元素操作的问题规模是链表的长度 n,而基本操作是指针 cur 的移动操作,所以查找元素算法的时间复杂度为 。

# 查找元素
def find(self, val):
    cur = self.head
    while cur:
        if val == cur.val:
            return cur
        cur = cur.next

    return None

1.2.4 插入元素

  • 链表头部插入元素:在链表第 1 个链节点之前插入值为 val 的链节点。
  • 链表尾部插入元素:在链表最后 1 个链节点之后插入值为 val 的链节点。
  • 链表中间插入元素:在链表第 i 个链节点之前插入值为 val 的链节点。

①链表头部插入元素
因为在链表头部插入链节点与链表的长度无关,所以该算法的时间复杂度为O(1)。

# 头部插入元素
def insertFront(self, val):
    node = ListNode(val)
    node.next = self.head
    self.head = node

如果构造的链表有哨兵节点,就需要让哨兵节点的next指向新创建的节点。

②链表尾部插入元素

  • 先创建一个值为 val 的链节点 node。
  • 使用指针 cur 指向链表的头节点 head。
  • 通过链节点的 next 指针移动 cur 指针,从而遍历链表,直到 cur.next == None。
  • 令 cur.next 指向将新的链节点 node。
def insertRear(self, val):
    node = ListNode(val)
    cur = self.head
    while cur.next:
        cur = cur.next
    cur.next = node

③中间插入元素

  • 使用指针变量 cur 和一个计数器 count。令 cur 指向链表的头节点,count 初始值赋值为 0。
  • 沿着链节点的 next 指针遍历链表,指针变量 cur 每指向一个链节点,计数器就做一次计数。
  • 当 count == index - 1 时,说明遍历到了第 index - 1 个链节点,此时停止遍历。
  • 创建一个值为 val 的链节点 node。
  • 将 node.next 指向 cur.next。
  • 然后令 cur.next 指向 node。

这里注意需要在所插入位置的前一位停下来,即index-1.

# 中间插入元素
def insertInside(self, index, val):
    count = 0
    cur = self.head
    while cur and count < index - 1:
        count += 1
        cur = cur.next
        
    if not cur:
        return 'Error'
    
    node = ListNode(val)
    node.next = cur.next
    cur.next = node

1.2.5 链表中间修改、删除元素

知道了如何插入元素后,修改和删除也同理。都是先通过head节点遍历到所需位置,再进行增删查改的操作。

二、707. 设计链表

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

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

2.1 题解

该题属于基础的链表构造题,安装上面提到的链表操作的思路写就行了。在下面的实现中,构造了哨兵节点。

class ListNode:

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

class MyLinkedList:

    def __init__(self):
        self.size = 0
        self.head = ListNode(0) #哨兵节点

    def get(self, index: int) -> int:
        if index > (self.size-1) or index < 0:
            return -1

        cur_node = self.head
        for i in range(0,index+1):
            cur_node = cur_node.next
        
        return cur_node.val

    def addAtHead(self, val: int) -> None:
        insert_node = ListNode(val)     #self.head是哨兵节点,应该存在它之后
        pred = self.head
        insert_node.next = pred.next
        pred.next = insert_node
        self.size += 1

    def addAtTail(self, val: int) -> None:
        self.addAtIndex(self.size,val)

    def addAtIndex(self, index: int, val: int) -> None:
        if index > self.size:
            return
        if index < 0:
            index = 0
        inser_node = ListNode(val)
        pred = self.head
        for i in range(index):
            pred = pred.next

        inser_node.next = pred.next
        pred.next = inser_node

        self.size += 1

    def deleteAtIndex(self, index: int) -> None:
        if index < 0 or index > (self.size-1):
            return
        pred = self.head
        for i in range(index):
            pred = pred.next
        
        pred.next = pred.next.next

        self.size -= 1


# Your MyLinkedList object will be instantiated and called as such:
# obj = MyLinkedList()
# param_1 = obj.get(index)
# obj.addAtHead(val)
# obj.addAtTail(val)
# obj.addAtIndex(index,val)
# obj.deleteAtIndex(index)

运行结果:
在这里插入图片描述
占用了比较多的内存,可能和多构建了个哨兵节点有关。

三、206.反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
在这里插入图片描述

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

3.1题解

从head节点开始,每次先需要储存前一个节点和后一个节点,即pred和next。然后让 cur遍历。
每次让cur的next指向pred,即实现反转。

class Solution:
    def reverseList(self, head: ListNode) -> ListNode:
        pred = None
        cur = head
        while (cur != None):
            next = cur.next
            cur.next = pred
            pred = cur
            cur = next

        return pred

这道题还是比较简单的,也不需要构造链表,只是写个Solution而已。
运行结果:
在这里插入图片描述

3.2 使用递归的解法

使用递归的方法进行求解:
假设有1-2-3-4-5的链表,使用递归时,需要先按1-2-3-4-5的顺序入栈,从5再一步步出栈,每次出栈修改节点的next指向。即node.next.next=node。实现反转,但此时注意到原先的next的联系未消除,需要使用node.next=None将原先的指向消除。

class Solution:
    def reverseList(self, head: ListNode) -> ListNode:
        if head == None or head.next == None:
            return head
        newhead = self.reverseList(head.next)
        head.next.next = head
        head.next = None
        return newhead

四、203.移除链表

题目描述:
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
在这里插入图片描述

输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]

4.1题解

需要使用另外一个指针,来处理链表第一个元素就被删除的情况,因为此时pred的指针还没赋值。

# Definition for singly-linked list.
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next
class Solution:
    def removeElements(self, head: ListNode, val: int) -> ListNode:
        cur = head
        star = ListNode(0,head)
        pred = star
        while(cur != None):
            if cur.val == val:
                pred.next = cur.next
                cur = cur.next
            else:
                pred = cur
                cur = cur.next

        return star.next

运行结果:
在这里插入图片描述

五、328.奇偶链表

题目描述:
给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。

请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。

示例:

输入: 1->2->3->4->5->NULL
输出: 1->3->5->2->4->NULL

分析:
由于题目要求原地完成,不能重新建立一个链表去实现。需要通过改变链表节点的next指向来实现。
o d d . n e x t = o d d . n e x t . n e x t odd.next = odd.next.next odd.next=odd.next.next
e v e n . n e x t = e v e n . n e x t . n e x t even.next = even.next.next even.next=even.next.next
并且由于每次循环都是改变一个奇数节点和一个偶数节点,可以通过偶数节点以及偶数节点的next是否为None来判断循环结束。

  • 当偶数节点为None时,对应链表数量为奇数,即 1 — — 2 — — 3 — — 4 — — 5 1——2——3——4——5 12345
  • 当偶数节点的next为None时,对应链表数量为偶数,即 1 — — 2 — — 3 — — 4 1——2——3——4 1234

5.1题解

class Solution:
    def oddEvenList(self, head: ListNode) -> ListNode:
        if head is None:
            return head

        even_head = head.next #记录偶节点开头
        odd = head
        even = even_head

        while even and even.next:
            odd.next = odd.next.next
            even.next = even.next.next

            odd = odd.next
            even = even.next
        odd.next = even_head

        return head

运行结果:
在这里插入图片描述

六、234.回文链表

题目描述:
给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。

在这里插入图片描述

示例:

输入:head = [1,2,2,1]
输出:true

分析:
这道题很直接的思路就是把链表的值存成数组,对数组操作就很简单了。原本是想通过将数组复制一份,再使用reverse()函数进行翻转,通过判断两个数组是否相等实现回文链表内容的判断。

但是,我发现只有deepcopy能实现深拷贝,所以在不引入copy模块的前提下,这个方法是行不通的。

因此,考虑使用前面学过的双指针的对撞指针进行解决,将first和last指针指向第一个元素和最后一个元素,然后判断它们是否相等,并更新两个指针的指向元素。

这种方法虽然思路简单,但其空间和时间复杂度都为 O ( N ) O(N) O(N)

代码示例:

def isPalindrome(head):
    value = []
    node = head
    while node != None:
        value.append(node.val)
        node = node.next
    first = value[0]
    last = value[-1]
    for i in range(len(value)//2):
        if first != last:
            return False
        first = value[0+i+1]
        last = value[-1-i-1]
    return True

运行结果:
在这里插入图片描述

七、链表排序

参考链接:链表排序

  • 适合链表的排序算法:冒泡排序、选择排序、插入排序、归并排序、快速排序、计数排序、桶排序、基数排序。
  • 不适合链表的排序算法:希尔排序。
  • 可以用于链表排序但不建议使用的排序算法:堆排序。

重点了解链表插入排序、链表归并排序

7.1链表归并排序

补充归并排序:

归并排序的思想:
采用经典的分治策略,先递归地将当前序列平均分成两半。然后将有序序列两两合并,最终合并成一个有序序列。

具体步骤:

  • 初始时,将待排序序列中的 n 个记录看成 n 个有序子序列(每个子序列总是有序的),每个子序列的长度均为 1。
  • 把当前序列组中有序子序列两两归并,完成一遍之后序列组里的排序序列个数减半,每个子序列的长度加倍。
  • 对长度加倍的有序子序列重复上面的操作,最终得到一个长度为 n 的有序序列。
class Solution:
    def merge(self, left_arr, right_arr):
        arr = []
        while left_arr and right_arr:
            if left_arr[0] <= right_arr[0]:
                arr.append(left_arr.pop(0))
            else:
                arr.append(right_arr.pop(0))
        while left_arr:
            arr.append(left_arr.pop(0))
        while right_arr:
            arr.append(right_arr.pop(0))
        return arr

    def mergeSort(self, arr):
        size = len(arr)
        if size < 2:
            return arr
        mid = len(arr) // 2
        left_arr, right_arr = arr[0: mid], arr[mid:]
        return self.merge(self.mergeSort(left_arr), self.mergeSort(right_arr))

    def sortArray(self, nums: List[int]) -> List[int]:
        return self.mergeSort(nums)

参考链接

链表归并排序:
算法步骤:

  • 分割环节:找到链表中心链节点,从中心节点将链表断开,并递归进行分割。

    • 使用快慢指针 fast = head.next、slow = head,让 fast 每次移动 2 步,slow 移动 1 步,移动到链表末尾,从而找到链表中心链节点,即 slow。
    • 从中心位置将链表从中心位置分为左右两个链表 left_head 和 right_head,并从中心位置将其断开,即 slow.next = None。
    • 对左右两个链表分别进行递归分割,直到每个链表中只包含一个链节点。
  • 归并环节:将递归后的链表进行两两归并,完成一遍后每个子链表长度加倍。重复进行归并操作,直到得到完整的链表。

    • 使用哑节点 dummy_head 构造一个头节点,并使用 cur 指向 dummy_head 用于遍历。
      比较两个链表头节点 left 和 right 的值大小。将较小的头节点加入到合并后的链表中。并向后移动该链表的头节点指针。
    • 然后重复上一步操作,直到两个链表中出现链表为空的情况。
    • 将剩余链表插入到合并中的链表中。
    • 将哑节点 dummy_dead 的下一个链节点 dummy_head.next 作为合并后的头节点返回。
def merge(self, left, right):
    # 归并环节
    dummy_head = ListNode(-1)
    cur = dummy_head
    while left and right:
        if left.val <= right.val:
            cur.next = left
            left = left.next
        else:
            cur.next = right
            right = right.next
        cur = cur.next
    
    if left:
        cur.next = left
    elif right:
        cur.next = right
        
    return dummy_head.next
    
def mergeSort(self, head: ListNode):
    # 分割环节
    if not head or not head.next:
        return head
    
    # 快慢指针找到中心链节点
    slow, fast = head, head.next
    while fast and fast.next:
        slow = slow.next 
        fast = fast.next.next 
    
    # 断开左右链节点
    left_head, right_head = head, slow.next 
    slow.next = None
    
    # 归并操作
    return self.merge(self.mergeSort(left_head), self.mergeSort(right_head))

八、148.排序链表

题目描述:

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。

在这里插入图片描述
示例:

输入:head = [4,2,1,3]
输出:[1,2,3,4]

分析:
按照上面归并排序的思路,对链表进行操作即可。所用的解决方法也是归并排序,代码思路基本参考上面所给归并排序的示例。

代码如下:

class Solution:
    def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        def merge(left,right):
            dummy_head = ListNode(0)
            cur = dummy_head
            while left and right:
                if left.val <= right.val:
                    cur.next = left
                    left = left.next
                else:
                    cur.next = right
                    right = right.next
                cur = cur.next
            
            if left:
                cur.next = left
            if right:
                cur.next = right
            
            return dummy_head.next

        def mergeSort(head):
            #快慢指针分割
            if not head or not head.next:
                return head
            
            #找到中点
            slow, fast = head, head.next
            while fast and fast.next:
                slow = slow.next
                fast = fast.next.next
            
            #断开连接
            left_head, right_head = head, slow.next
            slow.next = None

            #归并
            return merge(mergeSort(left_head),mergeSort(right_head)) 
        
        return mergeSort(head)

运行结果:
在这里插入图片描述

九、21.合并两个有序链表

题目描述:

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
在这里插入图片描述

示例:

输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

分析:
这道题比较简单,相当于遍历两个节点比大小即可。相当于归并排序中的merge的比较部分而已。

代码:

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next
class Solution:
    def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
        dummpy = ListNode(0)
        cur = dummpy
        while list1 and list2:
            if list1.val <= list2.val:
                cur.next = list1
                list1 = list1.next
            else:
                cur.next = list2
                list2 = list2.next
            cur = cur.next

        if list1:
            cur.next = list1
        if list2:
            cur.next = list2
        return dummpy.next

运行结果:
在这里插入图片描述

九.链表双指针

参考链接

9.1 双指针

双指针(Two Pointers):指的是在遍历元素的过程中,不是使用单个指针进行访问,而是使用两个指针进行访问,从而达到相应的目的。如果两个指针方向相反,则称为对撞时针。如果两个指针方向相同,则称为快慢指针。如果两个指针分别属于不同的数组 / 链表,则称为分离双指针

在单链表中,一般只会用到快慢指针分离双指针。其中链表的快慢指针又分为起点不一致的快慢指针步长不一致的快慢指针

9.2起点不一致的快慢指针

起点不一致的快慢指针:指的是两个指针从同一侧开始遍历链表,但是两个指针的起点不一样。 快指针 fast 比慢指针 slow 先走 n 步,直到快指针移动到链表尾端时为止。

具体步骤如下:

  • 使用两个指针 slow、fast。slow、fast 都指向链表的头节点,即:slow = head,fast = head。
  • 先将快指针向右移动 n 步。然后再同时向右移动快、慢指针。
  • 等到快指针移动到链表尾部(即 fast == Node)时跳出循环体。

代码如下:

slow = head
fast = head

while n:
    fast = fast.next
    n -= 1
while fast:
    fast = fast.next
    slow = slow.next

起点不一致的快慢指针主要用于找到链表中倒数第 k 个节点、删除链表倒数第 N 个节点等。

9.3步长不一致的慢指针

步长不一致的慢指针:指的是两个指针从同一侧开始遍历链表,两个指针的起点一样,但是步长不一致。例如,慢指针 slow 每次走 1 步,快指针 fast 每次走两步。直到快指针移动到链表尾端时为止。

具体步骤:

  • 使用两个指针 slow、fast。slow、fast 都指向链表的头节点。
  • 在循环体中将快、慢指针同时向右移动,但是快、慢指针的移动步长不一致。比如将慢指针每次移动 1 步,即 slow = slow.next。快指针每次移动 2 步,即 fast = fast.next.next。
  • 等到快指针移动到链表尾部(即 fast == Node)时跳出循环体。

代码:

fast = head
slow = head

while fast and fast.next:
    slow = slow.next
    fast = fast.next.next

步长不一致的快慢指针适合寻找链表的中点、判断和检测链表是否有环、找到两个链表的交点等问题。

十.019.删除链表的倒数第 N 个结点

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

在这里插入图片描述

示例:

输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

分析:
这道题使用链表快慢指针解决即可,快慢指针可以定位到想删除的结点位置。但需要注意的是,在这道题为了方便删除操作,将慢指针定位到待删除结点的前面一个结点。而且为了处理链表中只有一个结点的情况,建立一个哑结点dummpy,否则删除结点的操作slow.next = slow.next.next会出错。
另一种官方给的思路:先遍历一遍得到链表的大小,再遍历一遍找到相应的结点,这样需要遍历两次,而快慢指针只需要遍历一次。

代码:

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next
class Solution:
    def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
        dummpy = ListNode(0)
        dummpy.next = head
        slow = dummpy
        fast = dummpy

        while n:
            fast = fast.next
            n = n - 1
        
        while fast.next:
            fast = fast.next
            slow = slow.next

        slow.next = slow.next.next
        return dummpy.next

运行结果:
在这里插入图片描述

十一、876.链表的中间结点

题目描述:
给定一个头结点为 head 的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。

示例:

输入:[1,2,3,4,5]
输出:此列表中的结点 3 (序列化形式:[3,4,5])
返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。
注意,我们返回了一个 ListNode 类型的对象 ans,这样:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.

输入:[1,2,3,4,5,6]
输出:此列表中的结点 4 (序列化形式:[4,5,6])
由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。

分析:
使用步长不一致的快慢指针天然可以定位到链表的中间结点。

代码:

class Solution:
    def middleNode(self, head: ListNode) -> ListNode:
        slow = head
        fast = head
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
        return slow

运行结果:
在这里插入图片描述

参考链接:
算法手册
算法手册电子书
欢迎大家给作者star!

以上内容均参考自该电子书,侵联删。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值