大佬级数据结构与算法教程(小白能懂,代码可直接复制,全程实战)

数据结构与算法教程(小白能懂,代码可直接复制,全程实战)

这篇教程,从基础到进阶,覆盖80%面试/实战高频的数据结构与算法,每个知识点都配可直接复制运行的Python代码通俗案例解析避坑指南,全程无多余废话,小白跟着复制代码、理解逻辑,就能从0到1吃透,既能应对课程作业、期末考核,也能为后续面试、实战打下基础。

⚠️ 核心承诺:不用懂复杂的数学推导,不用死记硬背公式,只要会基础Python,就能看懂、会用,所有代码均亲测可运行,复制即执行!


一、开篇必看:数据结构与算法到底是什么?(一句话秒懂)

1. 大白话拆解核心概念

先抛弃晦涩定义,用生活案例讲明白,记死这两句话:

  • 数据结构:就是“存储数据的容器”,比如数组是“一排连续的盒子”,链表是“一串手拉手的珠子”,栈是“只能从顶端放、顶端拿的木桶”——核心是“怎么存数据更高效”。

  • 算法:就是“操作数据的步骤”,比如从数组里找一个数、给链表排序、从二叉树里查数据——核心是“怎么操作数据更快、更省空间”。

举个最通俗的例子:

你有100本课本,想存放在书架上(数据结构):

  • 按顺序排一排(数组):找某本书时,能快速定位,但要插入/删除一本书,需要挪动后面所有书(效率低);

  • 用绳子串起来挂在书架上(链表):插入/删除一本书很方便,直接解开绳子重新系,但找某本书时,需要从第一本开始依次找(效率低);

  • 算法就是“找书的步骤”:比如按书名首字母找(二分查找)、按编号找(顺序查找),不同步骤的效率天差地别。

2. 核心原则(小白必记)

学习数据结构与算法,不用追求“全学会”,重点抓3个核心,避免走弯路:

  1. 先学“高频基础”:优先掌握「数组、链表、栈、队列、哈希表、二叉树」,这6个是所有算法的基础,覆盖80%实战场景;

  2. 先懂“逻辑”,再写代码:比如先搞懂“链表怎么插入节点”,再复制代码运行,理解每一行代码的作用,而不是盲目复制;

  3. 多练“小案例”:每个知识点配1-2个实战案例,代码跑通后,自己改一改参数(比如改数组长度、改链表节点值),加深理解。

3. 前置准备(零门槛,小白必做)

不用安装复杂工具,只要做好2件事,就能开始学习:

  1. 环境:Python 3.8+(推荐3.10,兼容性最好),安装方式:官网下载,下一步下一步即可;

  2. 工具:任意代码编辑器(PyCharm、VS Code均可),新手推荐VS Code(轻量、免费、易操作);

  3. 核心:记住“代码可直接复制运行”,遇到报错,先看文末避坑指南,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) //

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值