本章我们将深入学习计算机科学中最核心、最基础的几种数据结构。掌握它们是构建高效算法的基石。我们将不仅学习它们的理论,更会亲手实现并分析其优劣。
数组是一种线性数据结构,它将相同类型的元素存储在连续的内存空间中。这使得数组具备一个强大的特性:可以通过索引(下标)在 O(1)
时间复杂度内随机访问任何元素。
O(1)
的时间复杂度。O(n)
。链表是另一种线性数据结构,但它的元素在内存中不一定是连续存放的。每个元素(称为“节点”)包含两部分:数据域和指向下一个节点的指针域。
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缓存 | 低,节点分散在各处 |
如何选择?
选择数组的场景:
选择链表的场景:
特性 | 数组 (Array) | 链表 (Linked List) |
---|---|---|
随机访问 | O(1) | O(n) |
插入/删除 | O(n) | O(1) |
内存空间 | 连续 | 分散 |
大小 | 固定 | 动态 |
示例: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)
二分查找是在有序数组中查找特定值的高效算法,时间复杂度为O(log n)。
算法流程图:
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 "未找到")
算法流程图:
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)
算法流程图:
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)
算法流程图(Floyd判圈算法):
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}")
栈和队列是两种特殊的、受限制的线性数据结构。
栈遵循后进先出 (Last-In, First-Out - LIFO) 原则。可以把它想象成一摞盘子,最后放上去的盘子最先被取走。
push
: 入栈,在顶部添加元素。pop
: 出栈,移除并返回顶部元素。队列遵循先进先出 (First-In, First-Out - FIFO) 原则。就像排队买票,最先来的人最先买到票。
enqueue
: 入队,在队尾添加元素。dequeue
: 出队,移除并返回队头元素。实现方式: 栈和队列都可以用数组(或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
示例:用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}")
算法流程图:
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
算法流程图:
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}")
算法流程图:
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
哈希表(又称散列表)是一种提供快速插入、删除和查找操作的数据结构。它的平均时间复杂度可以达到 O(1)
。
核心原理:
哈希冲突 (Hash Collision): 当两个不同的键经过哈希函数计算后得到相同的索引时,就发生了冲突。
一个好的哈希函数应尽可能地将键均匀地分布到数组的各个位置,以减少冲突。常见的设计方法有:
hash(key) = key % m
(m通常为质数)。hash(key) = floor(m * (key * A % 1))
(A是常数, 0 < A < 1)。1^2, -1^2, 2^2, -2^2...
的步长进行探测。可以缓解聚集现象。本章我们深入探讨了计算机科学中最核心的几种基础数据结构,它们是构建复杂算法的基石。核心知识点回顾如下:
数组 (Array) 与 链表 (Linked List):
O(1)
的随机访问速度著称,适用于数据量固定、读多写少的场景。但其插入和删除操作的 O(n)
复杂度是其主要短板。O(1)
的高效插入和删除能力(在已知节点时),且大小灵活。其代价是牺牲了随机访问能力(O(n)
)。栈 (Stack) 与 队列 (Queue):
哈希表 (Hash Table):
O(1)
时间复杂度的快速查找、插入和删除操作,是提升算法效率的利器。熟练掌握这些基础数据结构的原理、特性和适用场景,是你通往高级算法设计与分析的必经之路。请务必亲手实现它们,加深理解。