第2章:基础数据结构

本章我们将深入学习计算机科学中最核心、最基础的几种数据结构。掌握它们是构建高效算法的基石。我们将不仅学习它们的理论,更会亲手实现并分析其优劣。


2.1 数组 (Array) 与链表 (Linked List)

2.1.1 内容讲解

1. 数组 (Array)

数组是一种线性数据结构,它将相同类型的元素存储在连续的内存空间中。这使得数组具备一个强大的特性:可以通过索引(下标)在 O(1) 时间复杂度内随机访问任何元素。

  • 优点:
    • 随机访问速度快: O(1) 的时间复杂度。
  • 缺点:
    • 大小固定: 创建时需要指定大小,不易扩展。
    • 插入和删除效率低: 在数组中间插入或删除元素,需要移动后续所有元素,时间复杂度为 O(n)
2. 链表 (Linked List)

链表是另一种线性数据结构,但它的元素在内存中不一定是连续存放的。每个元素(称为“节点”)包含两部分:数据域和指向下一个节点的指针域。

  • 优点:
    • 大小灵活: 可以动态增长或缩减。
    • 插入和删除效率高: 只需改变相邻节点的指针,时间复杂度为 O(1)(如果已知要操作的节点)。
  • 缺点:
    • 随机访问速度慢: 无法通过索引直接访问,需要从头节点开始遍历,时间复杂度为 O(n)
    • 需要额外空间: 每个节点都需要存储指针,空间开销更大。

数组 vs. 链表:深度对比与场景选择

特性 数组 (Array) 链表 (Linked List)
数据结构类型 线性、连续内存 线性、非连续内存
随机访问 O(1) O(n)
插入/删除 (首/尾) O(n) / O(1) (动态数组) O(1)
插入/删除 (中间) O(n) O(n) (查找) + O(1) (操作)
内存空间 连续,可能预留空间造成浪费 分散,指针占用额外空间
缓存友好性 高,连续内存利于CPU缓存 低,节点分散在各处

如何选择?

  • 选择数组的场景

    • 需要频繁地通过索引访问元素。
    • 数据量固定,或变化不大。
    • 对内存连续性有要求,追求极致的访问速度(利用CPU缓存)。
  • 选择链表的场景

    • 需要频繁地插入和删除元素。
    • 数据量不确定,需要动态调整。
    • 对随机访问没有要求。
特性 数组 (Array) 链表 (Linked List)
随机访问 O(1) O(n)
插入/删除 O(n) O(1)
内存空间 连续 分散
大小 固定 动态

2.1.2 代码示例

示例:Python中链表的简单实现

class ListNode:
    """链表节点"""
    def __init__(self, value=0, next=None):
        self.value = value
        self.next = next

def print_linked_list(head):
    """遍历打印链表"""
    current = head
    result = []
    while current:
        result.append(str(current.value))
        current = current.next
    print("->".join(map(str, result)))

# 创建一个简单的链表: 1 -> 2 -> 3
head = ListNode(1)
head.next = ListNode(2)
head.next.next = ListNode(3)

print("原始链表:")
print_linked_list(head)

# 在链表头部插入一个新节点 0
new_head = ListNode(0, head)
print("插入新头节点后:")
print_linked_list(new_head)

2.1.3 核心算法实现

算法1: 二分查找 (Binary Search)

二分查找是在有序数组中查找特定值的高效算法,时间复杂度为O(log n)。

算法流程图:

开始:left=0, right=len(arr)-1
left <= right?
计算mid = (left+right)//2
arr[mid] == target?
返回mid
arr[mid] < target?
left = mid + 1
right = mid - 1
返回-1(未找到)
def binary_search(arr, target):
    """
    在有序数组中查找目标值
    
    Args:
        arr: 有序数组
        target: 目标值
    
    Returns:
        目标值的索引,如果不存在返回-1
    """
    left, right = 0, len(arr) - 1
    
    while left <= right:
        mid = (left + right) // 2
        
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    
    return -1

# 测试二分查找
arr = [1, 3, 5, 7, 9, 11, 13, 15]
target = 7
result = binary_search(arr, target)
print(f"在数组 {arr} 中查找 {target}")
print(f"结果: 索引 {result}" if result != -1 else "未找到")
算法2: 合并两个有序链表 (Merge Two Sorted Lists)

算法流程图:

开始:创建虚拟头节点dummy
current = dummy
list1 && list2 都存在?
list1.value <= list2.value?
current.next = list1
list1 = list1.next
current.next = list2
list2 = list2.next
current = current.next
list1 存在?
current.next = list1
current.next = list2
返回dummy.next
def merge_two_lists(list1, list2):
    """
    合并两个有序链表
    
    Args:
        list1: 第一个有序链表的头节点
        list2: 第二个有序链表的头节点
    
    Returns:
        合并后链表的头节点
    """
    # 创建虚拟头节点
    dummy = ListNode(0)
    current = dummy
    
    # 比较两个链表的节点值,选择较小的加入结果链表
    while list1 and list2:
        if list1.value <= list2.value:
            current.next = list1
            list1 = list1.next
        else:
            current.next = list2
            list2 = list2.next
        current = current.next
    
    # 将剩余的节点直接连接
    current.next = list1 if list1 else list2
    
    return dummy.next

# 测试合并两个有序链表
# 创建第一个链表: 1 -> 2 -> 4
list1 = ListNode(1)
list1.next = ListNode(2)
list1.next.next = ListNode(4)

# 创建第二个链表: 1 -> 3 -> 4
list2 = ListNode(1)
list2.next = ListNode(3)
list2.next.next = ListNode(4)

print("\n合并前:")
print("链表1:", end=" ")
print_linked_list(list1)
print("链表2:", end=" ")
print_linked_list(list2)

merged = merge_two_lists(list1, list2)
print("合并后:", end=" ")
print_linked_list(merged)
算法3: 反转链表 (Reverse Linked List)

算法流程图:

开始:prev = None, current = head
current 存在?
next_temp = current.next
current.next = prev
prev = current
current = next_temp
返回prev(新的头节点)
def reverse_linked_list(head):
    """
    反转单链表
    
    Args:
        head: 链表头节点
    
    Returns:
        反转后链表的头节点
    """
    prev = None
    current = head
    
    while current:
        next_temp = current.next  # 保存下一个节点
        current.next = prev       # 反转当前节点的指针
        prev = current           # 移动prev指针
        current = next_temp      # 移动current指针
    
    return prev  # prev现在指向新的头节点

# 测试反转链表
# 创建链表: 1 -> 2 -> 3 -> 4 -> 5
original = ListNode(1)
original.next = ListNode(2)
original.next.next = ListNode(3)
original.next.next.next = ListNode(4)
original.next.next.next.next = ListNode(5)

print("\n反转前:", end=" ")
print_linked_list(original)

reversed_list = reverse_linked_list(original)
print("反转后:", end=" ")
print_linked_list(reversed_list)
算法4: 检测链表环 (Detect Cycle in Linked List)

算法流程图(Floyd判圈算法):

开始:slow = head, fast = head
head 或 head.next 为空?
返回False
fast 和 fast.next 存在?
slow = slow.next
fast = fast.next.next
slow == fast?
返回True(有环)
返回False(无环)
def has_cycle(head):
    """
    检测链表是否有环(Floyd判圈算法 - 快慢指针)
    
    Args:
        head: 链表头节点
    
    Returns:
        True如果有环,False如果无环
    """
    if not head or not head.next:
        return False
    
    slow = head
    fast = head
    
    # 快指针每次走两步,慢指针每次走一步
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        
        # 如果快慢指针相遇,说明有环
        if slow == fast:
            return True
    
    return False

def detect_cycle_start(head):
    """
    找到链表环的起始节点
    
    Args:
        head: 链表头节点
    
    Returns:
        环的起始节点,如果无环返回None
    """
    if not head or not head.next:
        return None
    
    # 第一步:检测是否有环
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow == fast:
            break
    else:
        return None  # 无环
    
    # 第二步:找到环的起始点
    # 将一个指针重置到头部,两个指针同时以相同速度移动
    slow = head
    while slow != fast:
        slow = slow.next
        fast = fast.next
    
    return slow  # 环的起始节点

# 测试环检测
# 创建有环的链表: 1 -> 2 -> 3 -> 4 -> 2 (环)
cycle_list = ListNode(1)
cycle_list.next = ListNode(2)
cycle_list.next.next = ListNode(3)
cycle_list.next.next.next = ListNode(4)
cycle_list.next.next.next.next = cycle_list.next  # 形成环

print(f"\n链表是否有环: {has_cycle(cycle_list)}")
cycle_start = detect_cycle_start(cycle_list)
if cycle_start:
    print(f"环的起始节点值: {cycle_start.value}")

2.2 栈 (Stack) 与队列 (Queue)

栈和队列是两种特殊的、受限制的线性数据结构。

2.2.1 内容讲解

1. 栈 (Stack)

栈遵循后进先出 (Last-In, First-Out - LIFO) 原则。可以把它想象成一摞盘子,最后放上去的盘子最先被取走。

  • 核心操作:
    • push: 入栈,在顶部添加元素。
    • pop: 出栈,移除并返回顶部元素。
  • 应用: 函数调用栈、括号匹配、表达式求值等。
2. 队列 (Queue)

队列遵循先进先出 (First-In, First-Out - FIFO) 原则。就像排队买票,最先来的人最先买到票。

  • 核心操作:
    • enqueue: 入队,在队尾添加元素。
    • dequeue: 出队,移除并返回队头元素。
  • 应用: 任务调度、消息队列、广度优先搜索 (BFS) 等。

实现方式: 栈和队列都可以用数组(或Python中的列表)或链表来实现。使用数组实现简单直观,但可能面临动态扩容的性能开销;使用链表实现则可以提供真正的O(1)插入和删除操作,没有扩容问题。

示例:基于链表实现栈

class StackNode:
    def __init__(self, value):
        self.value = value
        self.next = None

class LinkedListStack:
    def __init__(self):
        self.top = None

    def push(self, value):
        new_node = StackNode(value)
        new_node.next = self.top
        self.top = new_node

    def pop(self):
        if self.top is None:
            return None
        popped_value = self.top.value
        self.top = self.top.next
        return popped_value

    def is_empty(self):
        return self.top is None

2.2.2 代码示例

示例:用Python列表模拟栈和队列

# 模拟栈 (LIFO)
stack = []
stack.append('A') # 入栈
stack.append('B')
stack.append('C')
print(f"栈: {stack}")
print(f"出栈元素: {stack.pop()}") # C出栈
print(f"出栈后: {stack}")

# 模拟队列 (FIFO)
from collections import deque

queue = deque()
queue.append('X') # 入队
queue.append('Y')
queue.append('Z')
print(f"\n队列: {queue}")
print(f"出队元素: {queue.popleft()}") # X出队
print(f"出队后: {queue}")

2.2.3 核心算法实现

算法5: 用两个栈实现队列 (Queue using Two Stacks)

算法流程图:

出队操作 (dequeue)
返回None
stack1和stack2都为空?
stack2为空?
将stack1所有元素转移到stack2
从stack2弹出元素
返回弹出的元素
入队操作 (enqueue)
入队完成
将元素压入stack1
class QueueUsingStacks:
    """
    使用两个栈实现队列
    
    思路:
    - stack1用于入队操作
    - stack2用于出队操作
    - 当需要出队时,如果stack2为空,将stack1的所有元素转移到stack2
    """
    
    def __init__(self):
        self.stack1 = []  # 用于入队
        self.stack2 = []  # 用于出队
    
    def enqueue(self, item):
        """入队操作"""
        self.stack1.append(item)
    
    def dequeue(self):
        """出队操作"""
        # 如果两个栈都为空,队列为空
        if not self.stack1 and not self.stack2:
            return None
        
        # 如果stack2为空,将stack1的元素全部转移到stack2
        if not self.stack2:
            while self.stack1:
                self.stack2.append(self.stack1.pop())
        
        # 从stack2弹出元素(队列的前端)
        return self.stack2.pop()
    
    def is_empty(self):
        """检查队列是否为空"""
        return len(self.stack1) == 0 and len(self.stack2) == 0
    
    def size(self):
        """返回队列大小"""
        return len(self.stack1) + len(self.stack2)

# 测试用两个栈实现的队列
queue = QueueUsingStacks()

# 入队操作
queue.enqueue(1)
queue.enqueue(2)
queue.enqueue(3)
print(f"\n入队1, 2, 3后,队列大小: {queue.size()}")

# 出队操作
print(f"出队: {queue.dequeue()}")  # 应该输出1
print(f"出队: {queue.dequeue()}")  # 应该输出2

# 继续入队
queue.enqueue(4)
print(f"入队4后,出队: {queue.dequeue()}")  # 应该输出3
print(f"出队: {queue.dequeue()}")  # 应该输出4
算法6: 有效的括号 (Valid Parentheses)

算法流程图:

开始:创建空栈stack
遍历字符串中的每个字符
字符是右括号?
栈为空 或 栈顶不匹配?
返回False
弹出栈顶元素
将左括号压入栈
还有字符?
栈为空?
返回True
返回False
def is_valid_parentheses(s):
    """
    检查括号字符串是否有效
    
    有效条件:
    1. 左括号必须用相同类型的右括号闭合
    2. 左括号必须以正确的顺序闭合
    
    Args:
        s: 只包含 '(', ')', '{', '}', '[', ']' 的字符串
    
    Returns:
        True如果字符串有效,False否则
    """
    # 栈用于存储左括号
    stack = []
    
    # 括号映射
    mapping = {
        ')': '(',
        '}': '{',
        ']': '['
    }
    
    for char in s:
        if char in mapping:  # 遇到右括号
            # 检查栈是否为空,或者栈顶元素是否匹配
            if not stack or stack.pop() != mapping[char]:
                return False
        else:  # 遇到左括号
            stack.append(char)
    
    # 最后栈应该为空
    return len(stack) == 0

# 测试有效括号
test_cases = [
    "()",           # True
    "()[]{}",       # True
    "(]",           # False
    "([)]",         # False
    "{[]}",         # True
    "",             # True (空字符串)
    "(((",          # False
    ")))",          # False
]

print("\n括号有效性检测:")
for test in test_cases:
    result = is_valid_parentheses(test)
    print(f"'{test}' -> {result}")
算法7: 滑动窗口最大值 (Maximum Value in Sliding Window)

算法流程图:

开始:创建双端队列dq
遍历数组元素
移除超出窗口范围的索引
移除队尾所有小于当前元素的索引
将当前索引加入队尾
窗口大小达到k?
记录队首索引对应的值(最大值)
还有元素?
返回结果数组
from collections import deque

def max_sliding_window(nums, k):
    """
    找到滑动窗口中的最大值
    
    使用双端队列(deque)来维护窗口中元素的索引,
    队列中的索引对应的元素值保持递减顺序
    
    Args:
        nums: 整数数组
        k: 滑动窗口大小
    
    Returns:
        每个窗口的最大值列表
    """
    if not nums or k == 0:
        return []
    
    dq = deque()  # 存储数组索引
    result = []
    
    for i in range(len(nums)):
        # 移除超出窗口范围的索引
        while dq and dq[0] <= i - k:
            dq.popleft()
        
        # 移除所有小于当前元素的索引
        # 保持队列中索引对应的值递减
        while dq and nums[dq[-1]] < nums[i]:
            dq.pop()
        
        dq.append(i)
        
        # 当窗口大小达到k时,记录最大值
        if i >= k - 1:
            result.append(nums[dq[0]])
    
    return result

# 测试滑动窗口最大值
nums = [1, 3, -1, -3, 5, 3, 6, 7]
k = 3
result = max_sliding_window(nums, k)
print(f"\n数组: {nums}")
print(f"窗口大小: {k}")
print(f"滑动窗口最大值: {result}")
# 输出: [3, 3, 5, 5, 6, 7]
# 解释: 
# [1  3  -1] -3  5  3  6  7  -> 3
#  1 [3  -1  -3] 5  3  6  7  -> 3
#  1  3 [-1  -3  5] 3  6  7  -> 5
#  1  3  -1 [-3  5  3] 6  7  -> 5
#  1  3  -1  -3 [5  3  6] 7  -> 6
#  1  3  -1  -3  5 [3  6  7] -> 7

2.3 哈希表 (Hash Table)

2.3.1 内容讲解

哈希表(又称散列表)是一种提供快速插入、删除和查找操作的数据结构。它的平均时间复杂度可以达到 O(1)

核心原理:

  1. 哈希函数 (Hash Function): 将输入的键 (Key) 转换为一个整数,这个整数就是数组的索引(哈希值)。
  2. 数组 (Array): 作为存储桶 (Bucket),存放数据。

哈希冲突 (Hash Collision): 当两个不同的键经过哈希函数计算后得到相同的索引时,就发生了冲突。

哈希函数的设计

一个好的哈希函数应尽可能地将键均匀地分布到数组的各个位置,以减少冲突。常见的设计方法有:

  • 除留余数法: hash(key) = key % m (m通常为质数)。
  • 乘法哈希法: hash(key) = floor(m * (key * A % 1)) (A是常数, 0 < A < 1)。
  • 对于字符串:通常会结合字符串中每个字符的ASCII码值来计算哈希值。
解决冲突的常用方法:
  • 链地址法 (Chaining): 在冲突的索引位置,用一个链表来存储所有映射到此处的键值对。
  • 开放地址法 (Open Addressing): 当发生冲突时,按照某种规则去寻找下一个可用的空位。所有元素都存放在哈希表数组内,不使用额外的数据结构。
    • 线性探测 (Linear Probing):从冲突位置开始,依次向后探测,直到找到空位。容易产生“聚集”现象。
    • 二次探测 (Quadratic Probing):从冲突位置开始,按照 1^2, -1^2, 2^2, -2^2... 的步长进行探测。可以缓解聚集现象。
    • 双重哈希 (Double Hashing):使用第二个哈希函数来计算探测的步长。能更好地避免聚集,分布更均匀。
哈希表示意图-链地址法
Hash Func
Hash Func
Hash Func
Index 1
Key1
Index 2
Key2
Key3
Bucket 1: Value1
Bucket 2: Node Key2,Val2 -> Node Key3,Val3

2.4 总结

本章我们深入探讨了计算机科学中最核心的几种基础数据结构,它们是构建复杂算法的基石。核心知识点回顾如下:

  1. 数组 (Array) 与 链表 (Linked List)

    • 数组:以其 O(1) 的随机访问速度著称,适用于数据量固定、读多写少的场景。但其插入和删除操作的 O(n) 复杂度是其主要短板。
    • 链表:提供了 O(1) 的高效插入和删除能力(在已知节点时),且大小灵活。其代价是牺牲了随机访问能力(O(n))。
    • 核心权衡:在“访问效率”与“增删效率”之间做选择是使用数组和链表的关键。
  2. 栈 (Stack) 与 队列 (Queue)

    • 栈 (LIFO):作为一种“后进先出”的结构,在函数调用、括号匹配等场景中扮演着重要角色。
    • 队列 (FIFO):作为一种“先进先出”的结构,是任务调度、广度优先搜索(BFS)等算法的核心。
    • 两者都是受限的线性结构,可以通过数组或链表实现,各有优劣。
  3. 哈希表 (Hash Table)

    • 提供了平均 O(1) 时间复杂度的快速查找、插入和删除操作,是提升算法效率的利器。
    • 核心在于哈希函数的设计和哈希冲突的解决。我们学习了两种主要的冲突解决方法:链地址法开放地址法

熟练掌握这些基础数据结构的原理、特性和适用场景,是你通往高级算法设计与分析的必经之路。请务必亲手实现它们,加深理解。

你可能感兴趣的:(算法那些事,数据结构)