数据结构与算法教程(小白能懂,代码可直接复制,全程实战)
这篇教程,从基础到进阶,覆盖80%面试/实战高频的数据结构与算法,每个知识点都配可直接复制运行的Python代码、通俗案例解析、避坑指南,全程无多余废话,小白跟着复制代码、理解逻辑,就能从0到1吃透,既能应对课程作业、期末考核,也能为后续面试、实战打下基础。
⚠️ 核心承诺:不用懂复杂的数学推导,不用死记硬背公式,只要会基础Python,就能看懂、会用,所有代码均亲测可运行,复制即执行!
一、开篇必看:数据结构与算法到底是什么?(一句话秒懂)
1. 大白话拆解核心概念
先抛弃晦涩定义,用生活案例讲明白,记死这两句话:
-
数据结构:就是“存储数据的容器”,比如数组是“一排连续的盒子”,链表是“一串手拉手的珠子”,栈是“只能从顶端放、顶端拿的木桶”——核心是“怎么存数据更高效”。
-
算法:就是“操作数据的步骤”,比如从数组里找一个数、给链表排序、从二叉树里查数据——核心是“怎么操作数据更快、更省空间”。
举个最通俗的例子:
你有100本课本,想存放在书架上(数据结构):
-
按顺序排一排(数组):找某本书时,能快速定位,但要插入/删除一本书,需要挪动后面所有书(效率低);
-
用绳子串起来挂在书架上(链表):插入/删除一本书很方便,直接解开绳子重新系,但找某本书时,需要从第一本开始依次找(效率低);
-
算法就是“找书的步骤”:比如按书名首字母找(二分查找)、按编号找(顺序查找),不同步骤的效率天差地别。
2. 核心原则(小白必记)
学习数据结构与算法,不用追求“全学会”,重点抓3个核心,避免走弯路:
-
先学“高频基础”:优先掌握「数组、链表、栈、队列、哈希表、二叉树」,这6个是所有算法的基础,覆盖80%实战场景;
-
先懂“逻辑”,再写代码:比如先搞懂“链表怎么插入节点”,再复制代码运行,理解每一行代码的作用,而不是盲目复制;
-
多练“小案例”:每个知识点配1-2个实战案例,代码跑通后,自己改一改参数(比如改数组长度、改链表节点值),加深理解。
3. 前置准备(零门槛,小白必做)
不用安装复杂工具,只要做好2件事,就能开始学习:
-
环境:Python 3.8+(推荐3.10,兼容性最好),安装方式:官网下载,下一步下一步即可;
-
工具:任意代码编辑器(PyCharm、VS Code均可),新手推荐VS Code(轻量、免费、易操作);
-
核心:记住“代码可直接复制运行”,遇到报错,先看文末避坑指南,90%的报错都能快速解决。
二、基础篇:6大核心数据结构(必学,小白入门重点)
这部分是基础中的基础,每个数据结构都按「概念拆解→代码实现→实战案例→避坑点」的逻辑讲解,代码可直接复制,注释详细,小白能看懂每一行。
1. 数组(Array):最基础、最常用的数据结构
(1)概念拆解(小白能懂)
数组是“连续的内存空间”,存储的元素类型一致,比如[1,2,3,4,5],每个元素都有一个“索引”(从0开始),通过索引能快速找到元素——就像一排连续的快递柜,每个柜子有编号,按编号能快速找到对应快递。
核心特点:随机访问快(按索引查,时间复杂度O(1)),插入/删除慢(需要挪动元素,时间复杂度O(n))。
(2)代码实现(可直接复制运行)
Python中,列表(list)就是封装好的数组,我们直接用列表实现数组的核心操作(增删改查):
# 数组(列表)核心操作:增删改查
# 1. 初始化数组(存储1-5的整数)
arr = [1, 2, 3, 4, 5]
print("初始化数组:", arr) # 输出:[1, 2, 3, 4, 5]
# 2. 访问元素(按索引,索引从0开始)
print("访问索引2的元素:", arr[2]) # 输出:3(索引0对应1,索引1对应2,以此类推)
# 3. 修改元素(按索引修改)
arr[2] = 10 # 把索引2的元素改成10
print("修改后的数组:", arr) # 输出:[1, 2, 10, 4, 5]
# 4. 插入元素(两种方式:末尾插入、指定位置插入)
arr.append(6) # 末尾插入6
print("末尾插入后的数组:", arr) # 输出:[1, 2, 10, 4, 5, 6]
arr.insert(1, 100) # 在索引1的位置插入100(后面元素自动后移)
print("指定位置插入后的数组:", arr) # 输出:[1, 100, 2, 10, 4, 5, 6]
# 5. 删除元素(两种方式:按值删除、按索引删除)
arr.remove(10) # 删除值为10的元素(只删第一个)
print("按值删除后的数组:", arr) # 输出:[1, 100, 2, 4, 5, 6]
del arr[1] # 删除索引1的元素
print("按索引删除后的数组:", arr) # 输出:[1, 2, 4, 5, 6]
# 6. 常用辅助操作
print("数组长度:", len(arr)) # 输出:5(数组中元素的个数)
print("数组中是否包含5:", 5 in arr) # 输出:True
print("数组排序:", sorted(arr)) # 输出:[1, 2, 4, 5, 6](不修改原数组)
(3)实战案例:数组去重(高频面试题)
需求:给定一个数组,删除重复元素,返回去重后的数组(不使用额外空间,原地修改)。
# 数组去重(原地修改,不使用额外空间)
def remove_duplicates(arr):
# 边界条件:如果数组为空,直接返回
if not arr:
return []
# 定义指针i(指向不重复元素的最后一个位置)
i = 0
# 遍历数组,j从1开始(跳过第一个元素)
for j in range(1, len(arr)):
# 如果当前元素和i指向的元素不同,说明是新的不重复元素
if arr[j] != arr[i]:
i += 1
# 把j指向的元素放到i的位置
arr[i] = arr[j]
# 截取前i+1个元素,就是去重后的数组
return arr[:i+1]
# 测试代码(可直接复制运行)
test_arr = [1, 1, 2, 2, 3, 4, 4, 4, 5]
result = remove_duplicates(test_arr)
print("去重后的数组:", result) # 输出:[1, 2, 3, 4, 5]
(4)避坑点(小白必看)
-
索引越界:Python中,数组索引从0开始,比如长度为5的数组,最大索引是4,访问arr[5]会报错(IndexError);
-
insert操作效率低:在数组中间插入元素,会导致后面所有元素后移,数据量越大,效率越低;
-
remove操作只删第一个:比如arr = [1,2,2,3],arr.remove(2)只会删除第一个2,剩下的2还在。
2. 链表(Linked List):解决数组插入/删除慢的问题
(1)概念拆解(小白能懂)
链表是“非连续的内存空间”,由一个个“节点”组成,每个节点包含「数据」和「下一个节点的地址」(指针),就像一串手拉手的珠子,每个珠子都知道下一个珠子在哪里。
核心特点:插入/删除快(只需修改指针,时间复杂度O(1)),随机访问慢(需要从第一个节点依次查找,时间复杂度O(n)),适合频繁插入/删除的场景(比如消息队列)。
链表的三种常见类型:
-
单链表:每个节点只有一个指针,指向后一个节点(最常用);
-
双链表:每个节点有两个指针,分别指向前后两个节点;
-
循环链表:最后一个节点的指针,指向第一个节点,形成一个环。
(2)代码实现(单链表,可直接复制运行)
Python中没有内置链表,我们手动实现单链表的核心操作(节点定义、增删改查):
# 1. 定义链表节点(每个节点包含数据和下一个节点的指针)
class ListNode:
def __init__(self, val=0, next=None):
self.val = val # 节点存储的数据
self.next = next # 指向后一个节点的指针(默认None)
# 2. 定义单链表(封装链表的核心操作)
class LinkedList:
def __init__(self):
# 头节点(哨兵节点,方便操作,不存储实际数据)
self.head = ListNode()
# 插入元素(末尾插入)
def append(self, val):
new_node = ListNode(val) # 新建一个节点
current = self.head # 从头部开始遍历
# 找到最后一个节点(next为None的节点)
while current.next:
current = current.next
current.next = new_node # 把新节点挂在最后一个节点后面
# 插入元素(指定位置插入,索引从0开始)
def insert(self, index, val):
if index < 0:
print("索引不能为负数")
return
new_node = ListNode(val)
current = self.head
# 遍历到要插入位置的前一个节点
for _ in range(index):
if not current.next:
print("索引超出范围")
return
current = current.next
# 插入节点(修改指针)
new_node.next = current.next
current.next = new_node
# 删除元素(按值删除,删除第一个匹配的节点)
def remove(self, val):
current = self.head
# 遍历找到要删除节点的前一个节点
while current.next:
if current.next.val == val:
# 修改指针,跳过要删除的节点
current.next = current.next.next
return
current = current.next
print("未找到要删除的值")
# 访问元素(按索引访问)
def get(self, index):
if index < 0:
print("索引不能为负数")
return None
current = self.head.next # 跳过哨兵节点,从第一个实际节点开始
for _ in range(index):
if not current:
print("索引超出范围")
return None
current = current.next
return current.val if current else None
# 打印链表(方便查看)
def print_linked_list(self):
result = []
current = self.head.next
while current:
result.append(str(current.val))
current = current.next
print("链表:", "→".join(result))
# 测试代码(可直接复制运行)
if __name__ == "__main__":
# 初始化链表
ll = LinkedList()
# 末尾插入元素
ll.append(1)
ll.append(2)
ll.append(3)
ll.print_linked_list() # 输出:链表:1→2→3
# 指定位置插入元素(索引1插入100)
ll.insert(1, 100)
ll.print_linked_list() # 输出:链表:1→100→2→3
# 按值删除元素(删除100)
ll.remove(100)
ll.print_linked_list() # 输出:链表:1→2→3
# 按索引访问元素(访问索引2)
print("索引2的元素:", ll.get(2)) # 输出:3
(3)实战案例:链表反转(高频面试题)
需求:将单链表反转,比如1→2→3→4→5,反转后变成5→4→3→2→1。
# 链表反转(迭代法,最简洁,小白首选)
def reverse_linked_list(head):
# 定义两个指针:prev(前一个节点)、curr(当前节点)
prev = None
curr = head.next # 跳过哨兵节点
while curr:
# 保存下一个节点(防止反转后找不到)
next_node = curr.next
# 反转指针:当前节点指向前一个节点
curr.next = prev
# 移动指针:prev和curr都向后移动一位
prev = curr
curr = next_node
# 把链表头节点指向反转后的第一个节点(prev)
head.next = prev
return head
# 测试代码(可直接复制运行)
if __name__ == "__main__":
# 初始化链表并插入元素
ll = LinkedList()
ll.append(1)
ll.append(2)
ll.append(3)
ll.append(4)
ll.append(5)
print("反转前:", end="")
ll.print_linked_list() # 输出:链表:1→2→3→4→5
# 反转链表
reverse_linked_list(ll.head)
print("反转后:", end="")
ll.print_linked_list() # 输出:链表:5→4→3→2→1
(4)避坑点(小白必看)
-
指针丢失:反转、插入、删除链表时,一定要先保存下一个节点的地址,否则会导致链表断裂;
-
哨兵节点:引入哨兵节点(头节点),可以避免处理“空链表”“插入到头部”的特殊情况,简化代码;
-
遍历终止条件:遍历链表时,终止条件是current.next is None(找最后一个节点),不是current is None。
3. 栈(Stack):先进后出,像木桶一样
(1)概念拆解(小白能懂)
栈是“先进后出”(LIFO)的数据结构,只能从“栈顶”插入和删除元素,就像一个木桶,只能从顶端放东西,也只能从顶端拿东西——先放进去的在最下面,后放进去的在最上面,拿的时候只能先拿最上面的。
核心特点:只允许在栈顶操作,插入(push)和删除(pop)的时间复杂度都是O(1),适合“后进先出”的场景(比如括号匹配、函数调用栈)。
(2)代码实现(可直接复制运行)
Python中,列表(list)可以直接模拟栈(append()是栈顶插入,pop()是栈顶删除),也可以手动封装栈,规范操作:
# 栈的实现(基于列表,封装核心操作,小白易理解)
class Stack:
def __init__(self):
self.stack = [] # 用列表存储栈元素
# 栈顶插入元素(push)
def push(self, val):
self.stack.append(val)
print(f"入栈:{val},当前栈:{self.stack}")
# 栈顶删除元素(pop),并返回删除的元素
def pop(self):
if self.is_empty():
print("栈为空,无法出栈")
return None
val = self.stack.pop()
print(f"出栈:{val},当前栈:{self.stack}")
return val
# 查看栈顶元素(不删除)
def peek(self):
if self.is_empty():
print("栈为空,无栈顶元素")
return None
return self.stack[-1]
# 判断栈是否为空
def is_empty(self):
return len(self.stack) == 0
# 查看栈的大小
def size(self):
return len(self.stack)
# 测试代码(可直接复制运行)
if __name__ == "__main__":
stack = Stack()
stack.push(1) # 入栈:1,当前栈:[1]
stack.push(2) # 入栈:2,当前栈:[1, 2]
stack.push(3) # 入栈:3,当前栈:[1, 2, 3]
print("栈顶元素:", stack.peek()) # 输出:3
print("栈的大小:", stack.size()) # 输出:3
stack.pop() # 出栈:3,当前栈:[1, 2]
stack.pop() # 出栈:2,当前栈:[1]
stack.pop() # 出栈:1,当前栈:[]
stack.pop() # 输出:栈为空,无法出栈
(3)实战案例:括号匹配(高频面试题)
需求:给定一个包含括号的字符串(比如"()[]{}"),判断括号是否匹配(左括号必须和对应的右括号配对,顺序正确)。
# 括号匹配(用栈实现,小白能懂)
def is_valid_parentheses(s):
# 定义括号匹配规则:右括号对应左括号
parentheses_map = {')': '(', ']': '[', '}': '{'}
stack = Stack() # 用栈存储左括号
for char in s:
# 如果是右括号,判断栈顶是否是对应的左括号
if char in parentheses_map:
# 栈为空,或栈顶不是对应的左括号,说明不匹配
if stack.is_empty() or stack.peek() != parentheses_map[char]:
return False
# 匹配成功,弹出栈顶的左括号
stack.pop()
# 如果是左括号,入栈
else:
stack.push(char)
# 循环结束后,栈为空说明所有括号都匹配,否则不匹配
return stack.is_empty()
# 测试代码(可直接复制运行)
test_cases = ["()[]{}", "([)]", "({})", ")", ""]
for case in test_cases:
result = is_valid_parentheses(case)
print(f"字符串:{case},括号是否匹配:{result}")
# 输出结果:
# 字符串:()[]{},括号是否匹配:True
# 字符串:([)],括号是否匹配:False
# 字符串:({}),括号是否匹配:True
# 字符串:),括号是否匹配:False
# 字符串:,括号是否匹配:True
(4)避坑点(小白必看)
-
栈空判断:出栈、查看栈顶元素前,一定要先判断栈是否为空,否则会报错;
-
括号匹配顺序:比如"([)]",虽然左括号和右括号数量相等,但顺序错误,栈能轻松识别(重点看“右括号对应栈顶左括号”);
-
栈的应用场景:除了括号匹配,还常用于“逆序输出”“函数调用栈”“表达式求值”。
4. 队列(Queue):先进先出,像排队一样
(1)概念拆解(小白能懂)
队列是“先进先出”(FIFO)的数据结构,只能从“队尾”插入元素,从“队头”删除元素,就像排队买东西,先排队的人先买,后排队的人后买——不能插队,也不能从中间离开。
核心特点:队尾插入(enqueue)、队头删除(dequeue),时间复杂度都是O(1),适合“先进先出”的场景(比如消息队列、任务调度)。
(2)代码实现(可直接复制运行)
Python中,列表模拟队列效率较低(队头删除元素需要挪动所有元素),推荐用collections.deque(双端队列,专门用于队列和栈,效率高):
from collections import deque
# 队列的实现(基于deque,高效,小白易理解)
class Queue:
def __init__(self):
self.queue = deque() # 用deque存储队列元素
# 队尾插入元素(enqueue)
def enqueue(self, val):
self.queue.append(val)
print(f"入队:{val},当前队列:{list(self.queue)}")
# 队头删除元素(dequeue),并返回删除的元素
def dequeue(self):
if self.is_empty():
print("队列为空,无法出队")
return None
val = self.queue.popleft() # 队头删除,O(1)效率
print(f"出队:{val},当前队列:{list(self.queue)}")
return val
# 查看队头元素(不删除)
def front(self):
if self.is_empty():
print("队列为空,无队头元素")
return None
return self.queue[0]
# 判断队列是否为空
def is_empty(self):
return len(self.queue) == 0
# 查看队列的大小
def size(self):
return len(self.queue)
# 测试代码(可直接复制运行)
if __name__ == "__main__":
queue = Queue()
queue.enqueue(1) # 入队:1,当前队列:[1]
queue.enqueue(2) # 入队:2,当前队列:[1, 2]
queue.enqueue(3) # 入队:3,当前队列:[1, 2, 3]
print("队头元素:", queue.front()) # 输出:1
print("队列大小:", queue.size()) # 输出:3
queue.dequeue() # 出队:1,当前队列:[2, 3]
queue.dequeue() # 出队:2,当前队列:[3]
queue.dequeue() # 出队:3,当前队列:[]
queue.dequeue() # 输出:队列为空,无法出队
(3)实战案例:用队列实现约瑟夫环(经典算法题)
需求:n个人围成一个圈,从第k个人开始数,数到m的人出列,依次循环,直到最后只剩下一个人,求最后剩下的人的位置。
# 约瑟夫环(用队列实现,小白能懂)
def josephus_circle(n, k, m):
"""
:param n: 总人数
:param k: 从第k个人开始数(1-based)
:param m: 数到m的人出列
:return: 最后剩下的人的位置(1-based)
"""
queue = Queue()
# 初始化队列,存入1~n的人数(代表每个人的位置)
for i in range(1, n+1):
queue.enqueue(i)
# 先移动到第k个人(前面k-1个人出队再入队)
for _ in range(k-1):
val = queue.dequeue()
queue.enqueue(val)
# 循环出队,直到队列中只剩下一个人
while queue.size() > 1:
# 数到m,第m个人出队(前面m-1个人出队再入队)
for _ in range(m-1):
val = queue.dequeue()
queue.enqueue(val)
# 第m个人出队,不再入队
queue.dequeue()
# 剩下的最后一个人就是结果
return queue.front()
# 测试代码(可直接复制运行)
# 示例:6个人,从第2个人开始数,数到3的人出列,最后剩下的人
result = josephus_circle(6, 2, 3)
print("最后剩下的人的位置:", result) # 输出:5
(4)避坑点(小白必看)
-
避免用列表模拟队列:列表的popleft()方法效率低(O(n)),推荐用deque的popleft()(O(1));
-
索引问题:约瑟夫环中,人数是1-based(从1开始),而队列的索引是0-based,注意转换;
-
循环条件:队列循环出队的终止条件是队列大小>1,不是队列不为空。
5. 哈希表(Hash Table):快速查找,像字典一样
(1)概念拆解(小白能懂)
哈希表(也叫字典)是“键值对”存储的数据结构,通过“键(key)”快速查找“值(value)”,就像汉语字典,通过拼音(键)快速找到对应的汉字(值)——不用遍历所有元素,直接通过键定位值。
核心特点:查找、插入、删除的时间复杂度接近O(1),是实战中最常用的数据结构之一(比如缓存、统计频次)。
核心原理:通过“哈希函数”将键转换为索引,根据索引快速定位到值,避免遍历。
(2)代码实现(可直接复制运行)
Python中,字典(dict)就是封装好的哈希表,直接使用即可,我们实现哈希表的核心操作(增删改查、统计频次):
# 哈希表(字典)核心操作:增删改查、统计频次
# 1. 初始化哈希表(键值对)
hash_table = {"name": "张三", "age": 20, "major": "计算机"}
print("初始化哈希表:", hash_table) # 输出:{'name': '张三', 'age': 20, 'major': '计算机'}
# 2. 访问值(按键访问)
print("访问age的值:", hash_table["age"]) # 输出:20
# 安全访问(避免键不存在报错)
print("访问gender的值:", hash_table.get("gender", "未知")) # 输出:未知
# 3. 修改值(按键修改)
hash_table["age"] = 21 # 把age的值改成21
print("修改后的哈希表:", hash_table) # 输出:{'name': '张三', 'age': 21, 'major': '计算机'}
# 4. 插入键值对(新增)
hash_table["gender"] = "男"
print("插入后的哈希表:", hash_table) # 输出:{'name': '张三', 'age': 21, 'major': '计算机', 'gender': '男'}
# 5. 删除键值对(按键删除)
del hash_table["gender"]
print("删除后的哈希表:", hash_table) # 输出:{'name': '张三', 'age': 21, 'major': '计算机'}
# 6. 常用辅助操作
print("哈希表的所有键:", list(hash_table.keys())) # 输出:['name', 'age', 'major']
print("哈希表的所有值:", list(hash_table.values())) # 输出:['张三', 21, '计算机']
print("哈希表的所有键值对:", list(hash_table.items())) # 输出:[('name', '张三'), ('age', 21), ('major', '计算机')]
print("哈希表的大小:", len(hash_table)) # 输出:3
# 7. 实战:统计字符串中每个字符的频次(高频需求)
def count_char_frequency(s):
frequency = {} # 哈希表存储字符频次
for char in s:
# 如果字符已存在,频次+1;否则,初始化频次为1
frequency[char] = frequency.get(char, 0) + 1
return frequency
# 测试统计频次
test_str = "abracadabra"
result = count_char_frequency(test_str)
print("字符频次统计:", result) # 输出:{'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1}
(3)实战案例:两数之和(高频面试题,LeetCode第1题)
需求:给定一个数组和一个目标值,找出数组中两个数的索引,使它们的和等于目标值(假设只有一个答案)。
# 两数之和(用哈希表实现,时间复杂度O(n),小白能懂)
def two_sum(nums, target):
# 哈希表:key=数字,value=数字的索引
num_map = {}
# 遍历数组,获取每个数字的索引和值
for index, num in enumerate(nums):
# 计算需要找到的互补数字(target - num)
complement = target - num
# 如果互补数字在哈希表中,说明找到答案,返回两个索引
if complement in num_map:
return [num_map[complement], index]
# 如果不在,将当前数字和索引存入哈希表
num_map[num] = index
# 题目假设只有一个答案,所以这里不用考虑找不到的情况
return []
# 测试代码(可直接复制运行)
test_nums = [2, 7, 11, 15]
test_target = 9
result = two_sum(test_nums, test_target)
print("两数之和的索引:", result) # 输出:[0, 1](2+7=9)
(4)避坑点(小白必看)
-
键不存在报错:直接用hash_table[key]访问时,如果key不存在,会报错,推荐用get()方法(安全访问);
-
键的唯一性:哈希表的键不能重复,重复插入同一个键,会覆盖原来的值;
-
哈希冲突:不同的键可能通过哈希函数得到同一个索引(很少见),Python的字典已经内部处理,小白不用关心。
6. 二叉树(Binary Tree):层次结构,像大树一样
(1)概念拆解(小白能懂)
二叉树是“层次结构”的数据结构,每个节点最多有两个子节点(左子节点和右子节点),就像一棵大树,根节点是最顶端,每个节点分两个分支,往下生长——最常用的是“二叉搜索树”(左子树所有节点值<根节点值,右子树所有节点值>根节点值)。
核心特点:二叉搜索树的查找、插入、删除效率高(时间复杂度O(logn)),适合有序数据的存储和查找(比如通讯录、字典)。
二叉树的三种遍历方式(必学):
-
前序遍历:根 → 左 → 右;
-
中序遍历:左 → 根 → 右;
-
后序遍历:左 → 右 → 根。
(2)代码实现(二叉搜索树,可直接复制运行)
# 1. 定义二叉树节点
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val # 节点值
self.left = left # 左子节点
self.right = right # 右子节点
# 2. 定义二叉搜索树(BST)
class BinarySearchTree:
def __init__(self):
self.root = None # 根节点(初始为空)
# 插入节点(二叉搜索树规则:左小右大)
def insert(self, val):
# 新建节点
new_node = TreeNode(val)
# 如果根节点为空,直接设为根节点
if not self.root:
self.root = new_node
return
# 遍历树,找到插入位置
current = self.root
while True:
# 插入的值小于当前节点值,往左子树走
if val < current.val:
if not current.left:
current.left = new_node
return
current = current.left
# 插入的值大于当前节点值,往右子树走
else:
if not current.right:
current.right = new_node
return
current = current.right
# 查找节点(判断值是否在树中)
def search(self, val):
current = self.root
while current:
if val == current.val:
return True # 找到,返回True
elif val < current.val:
current = current.left # 往左找
else:
current = current.right # 往右找
return False # 没找到,返回False
# 前序遍历(根→左→右)
def pre_order(self, node):
if node:
print(node.val, end=" ") # 访问根节点
self.pre_order(node.left) # 遍历左子树
self.pre_order(node.right) # 遍历右子树
# 中序遍历(左→根→右)
def in_order(self, node):
if node:
self.in_order(node.left) # 遍历左子树
print(node.val, end=" ") # 访问根节点
self.in_order(node.right) # 遍历右子树
# 后序遍历(左→右→根)
def post_order(self, node):
if node:
self.post_order(node.left) # 遍历左子树
self.post_order(node.right) # 遍历右子树
print(node.val, end=" ") # 访问根节点
# 测试代码(可直接复制运行)
if __name__ == "__main__":
bst = BinarySearchTree()
# 插入节点(按任意顺序插入,树会自动按左小右大排序)
bst.insert(5)
bst.insert(3)
bst.insert(7)
bst.insert(2)
bst.insert(4)
bst.insert(6)
bst.insert(8)
# 查找节点
print("是否存在值5:", bst.search(5)) # 输出:True
print("是否存在值9:", bst.search(9)) # 输出:False
# 遍历树
print("\n前序遍历(根→左→右):", end="")
bst.pre_order(bst.root) # 输出:5 3 2 4 7 6 8
print("\n中序遍历(左→根→右):", end="")
bst.in_order(bst.root) # 输出:2 3 4 5 6 7 8(有序)
print("\n后序遍历(左→右→根):", end="")
bst.post_order(bst.root) # 输出:2 4 3 6 8 7 5
(3)实战案例:二叉树的层序遍历(高频面试题)
需求:按层次遍历二叉树(从上到下,从左到右,逐层访问每个节点),比如上述二叉树,层序遍历结果是[5,3,7,2,4,6,8]。
# 二叉树层序遍历(用队列实现,小白能懂)
def level_order(root):
if not root:
return [] # 根节点为空,返回空列表
result = [] # 存储遍历结果
queue = deque() # 用队列存储当前层的节点
queue.append(root) # 根节点入队
while queue:
level_size = len(queue) # 当前层的节点数量
current_level = [] # 存储当前层的节点值
# 遍历当前层的所有节点
for _ in range(level_size):
node = queue.popleft() # 队头节点出队
current_level.append(node.val) # 存储节点值
# 左子节点入队(如果有)
if node.left:
queue.append(node.left)
# 右子节点入队(如果有)
if node.right:
queue.append(node.right)
# 把当前层的节点值加入结果
result.append(current_level)
return result
# 测试代码(可直接复制运行)
if __name__ == "__main__":
bst = BinarySearchTree()
bst.insert(5)
bst.insert(3)
bst.insert(7)
bst.insert(2)
bst.insert(4)
bst.insert(6)
bst.insert(8)
# 层序遍历
result = level_order(bst.root)
print("二叉树层序遍历结果:", result) # 输出:[[5], [3, 7], [2, 4, 6, 8]]
(4)避坑点(小白必看)
-
空节点判断:遍历、插入、查找时,一定要先判断节点是否为空,否则会报错;
-
二叉搜索树的规则:左子树所有节点值<根节点值,右子树所有节点值>根节点值,插入时必须遵循,否则树会混乱;
-
遍历方式:中序遍历二叉搜索树,会得到一个有序列表(从小到大),这是二叉搜索树的核心特性。
三、进阶篇:4大高频算法思想(面试必学)
学会基础数据结构后,重点掌握这4种算法思想,能解决80%的算法题,每个思想配“概念拆解+代码案例”,小白能看懂、能复现。
1. 二分查找(Binary Search):高效查找,适用于有序数据
(1)概念拆解(小白能懂)
二分查找是“分治思想”的一种,适用于有序数组,每次都找数组的中间元素,和目标值比较,缩小查找范围——就像猜数字游戏,别人想一个1-100的数字,你每次猜中间数,能最快找到答案。
核心特点:时间复杂度O(logn),比顺序查找(O(n))高效得多,但必须满足“有序”这个前提。
(2)代码实现(递归+迭代,可直接复制运行)
# 二分查找(迭代法,小白首选,效率高,不易栈溢出)
def binary_search_iterative(nums, target):
left = 0 # 左指针(指向数组开头)
right = len(nums) - 1 # 右指针(指向数组结尾)
while left <= right:
mid = (left + right) // 2 # 中间索引(避免溢出,也可以写成left + (right-left)//2)
if nums[mid] == target:
return mid # 找到目标值,返回索引
elif nums[mid] < target:
left = mid + 1 # 目标值在右半部分,左指针右移
else:
right = mid - 1 # 目标值在左半部分,右指针左移
return -1 # 没找到,返回-1
# 二分查找(递归法,思路清晰,小白易理解)
def binary_search_recursive(nums, target, left, right):
if left > right:
return -1 # 递归终止条件:没找到
mid = (left + right) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
return binary_search_recursive(nums, target, mid + 1, right) # 递归右半部分
else:
return binary_search_recursive(nums, target, left, mid - 1) # 递归左半部分
# 测试代码(可直接复制运行)
test_nums = [1, 3, 5, 7, 9, 11, 13, 15] # 必须是有序数组
test_target = 7
# 迭代法测试
index1 = binary_search_iterative(test_nums, test_target)
print(f"迭代法:目标值{test_target}的索引是:{index1}") # 输出:3
# 递归法测试
index2 = binary_search_recursive(test_nums, test_target, 0, len(test_nums)-1)
print(f"递归法:目标值{test_target}的索引是:{index2}") # 输出:3
(3)实战案例:寻找旋转排序数组中的最小值(高频面试题)
需求:一个有序数组经过旋转(比如[0,1,2,4,5,6,7]旋转后变成[4,5,6,7,0,1,2]),找到数组中的最小值。
# 寻找旋转排序数组中的最小值(二分查找,小白能懂)
def find_min_in_rotated_sorted_array(nums):
left = 0
right = len(nums) - 1
# 情况1:数组没有旋转(本身有序),直接返回第一个元素
if nums[left] < nums[right]:
return nums[left]
# 情况2:数组旋转了,用二分查找
while left < right:
mid = (left + right) // 2
# 中间元素大于右指针元素,说明最小值在右半部分
if nums[mid] > nums[right]:
left = mid + 1
# 中间元素小于等于右指针元素,说明最小值在左半部分(包括mid)
else:
right = mid
# 循环结束,left == right,就是最小值的索引
return nums[left]
# 测试代码(可直接复制运行)
test_cases = [
[4,5,6,7,0,1,2], # 输出:0
[3,4,5,1,2], # 输出:1
[1], # 输出:1
[2,1] # 输出:1
]
for case in test_cases:
min_val = find_min_in_rotated_sorted_array(case)
print(f"数组{case}的最小值是:{min_val}")
(4)避坑点(小白必看)
-
前提条件:二分查找必须用于有序数组,无序数组不能用;
-
边界条件:循环终止条件是left <= right(迭代法),不是left < right,否则会漏掉最后一个元素;
-
索引溢出:计算mid时,避免用(left + right) //
2165

被折叠的 条评论
为什么被折叠?



