数据结构3 线性表 - 链表

60 阅读4分钟

链表
基本信息:

  1. 通过指针将一组零散的内存块串联起来(就像是排队,你知道你后面的人是谁)
  2. 结点(Value):内存块 头结点, 尾结点
  3. 后继指针(next): 记录下个结点地址的指针

特点:

  1. 插入快,只需要考虑相邻接点的指针改变。O(1)
  2. 随机访问慢,因为要一个个遍历结点, 无法通过偏移量查找数据。O(n)

时间复杂度
Lookup O(n)
Insert O(1)
Delete O(1)
Append O(1)
Prepend O(1)

空间复杂度
O(n)

Code eg:
Java LinkedList

单链表
尾结点指向null

循环链表
尾结点指向头结点 从链尾到链头比较方便,需要特殊的环形结构时就可以使用

双链表
更占空间,空间换时间
每个结点同时拥有前驱结点和后继结点
删除插入更快
如果是有序列表,那么按值查找的速度也更快,可以记录一个位置p,每次查询时看看查找值与p的大小,只要查找一半的数据

保护结点
在进行数组操作时我们要注意数组越界,而在进行链表操作时,我们要注意空值判断,我们可以针对单双链表设置保护结点来减少判断。

  1. 空单链表: 构造一个头结点,next指向null
  2. 空双链表: 构造头尾结点,头结点next指向尾结点,pre指向null,尾结点pre指向头结点,next指向null。

实战

1.反转链表
leetcode.cn/problems/re…

思路

  1. 遍历链表,边界值考虑: node是否为null
  2. 记录两个结点,当前结点head和上一个结点last
  3. 将当前结点指向上一个结点
  4. 当前结点和上一个结点同时前进一位,继续反转
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def reverseList(self, head: ListNode) -> ListNode:
        last = None
        while head:
            # 记录当前节点的下一个节点
            next_head = head.next
            # 当前结点指向前一个结点
            head.next = last
            #当前结点和前一个结点同时往后移动一位
            last = head
            head = next_head
        return last

2.K个一组翻转链表
leetcode.cn/problems/re…

思路

  1. 分组遍历,往后走k-1步找到一组,记录一组的end
  2. 每组内部反转,边界判断:head != end.next
  3. 更新每组跟前一组和后一组的边
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:

    #返回k-1步后的结点
    #返回None说明不足k
    def get_end(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
        while head:
            k -= 1
            #往后走k步
            if k == 0:
                return head
            head = head.next
        return None

    #每组内部反转
    def reverse_list(self, head: Optional[ListNode], stop: Optional[ListNode]) :
        last = head
        #直接从head.next开始, 内部反转
        head = head.next
        while head != stop:
            next_head = head.next
            head.next = last
            last = head
            head = next_head 
        
    def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:       
        #定义一个保护结点,也就是访问入口,指向head
        protect = ListNode(0, head)
        #将第一组赋值给上一组
        last = protect

        while head:
            #分组, 获取每组的end结点
            end = self.get_end(head, k)
            if end == None:
                break
            next_group_head = end.next

            #每组之间的反转
            self.reverse_list(head, next_group_head)
            
            #更新当前组和前一组,后一组的边
            last.next = end
            head.next = next_group_head

            #上一组和当前组同时移动k步长
            last = head
            head = next_group_head

        return protect.next

3.邻值查找
www.acwing.com/problem/con…

4.环形链表
leetcode.cn/problems/li…

思路

1.哈希表

class Solution:
    def hasCycle(self, head: Optional[ListNode]) -> bool:
        unique_table = set()
        while head:
            if head in unique_table:
                return True
            unique_table.add(head)
            head = head.next
        return False

2.快慢指针 O(m)时间复杂度

如果有环必相遇,无环快指针走到None

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

5.环形链表2
leetcode.cn/problems/li…

思路
快慢指针

  1. 首先判断是否有环
  2. 有环则慢指针到入环点的距离等于head到入环点的距离
  3. 同时让head和slow移动,必定在起始点相遇
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None

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

6.合并两个有序链表
leetcode.cn/problems/me…

思路

  1. 终止条件:其中一个链表为空时,返回另一个链表
  2. 判断 l1 和 l2 头结点哪个更小,然后较小结点的 next 指针指向其余结点的合并结果。(调用递归)
# Definition for singly-linked list.
# 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]:
        if not list1: return list2  # 终止条件,有一个链表为空
        if not list2: return list1
        if list1.val <= list2.val:  # 递归调用
            #谁小,将小的结点指向后续需要排序的列表
            list1.next = self.mergeTwoLists(list1.next,list2)
            return list1
        else:
            list2.next = self.mergeTwoLists(list1,list2.next)
            return list2

7.加一
leetcode.cn/problems/pl…

思路

  1. 末位无进位,则末位加一即可,因为末位无进位,前面也不可能产生进位,比如 45 => 46
  2. 末位有进位,在中间位置进位停止,则需要找到进位的典型标志,即为当前位 %10后为 0,则前一位加 1,直到不为 0 为止,比如 499 => 500
  3. 末位有进位,并且一直进位到最前方导致结果多出一位,对于这种情况,需要在第 2 种情况遍历结束的基础上,进行单独处理,比如 999 => 1000
class Solution:
    def plusOne(self, digits: List[int]) -> List[int]:
        n = len(digits)
        #逆序遍历
        for i in range(n - 1, -1, -1):
            digits[i] += 1
            #无进位情况,直接返回
            if digits[i] < 10:
                return digits
            else:
                #有进位,对10取余
                digits[i] %= 10
        #进位        
        return [1] + digits