同学们,我们继续Python编程基础的学习。上一节我们已经掌握了Python的核心语法,包括变量、运算符和流程控制。现在,我们要把目光投向Python中非常重要的一部分——常用数据结构。
大家可以把数据结构想象成不同的“收纳盒”或“容器”,每种“收纳盒”都有其独特的存储方式和使用规则。高效地组织和管理数据,是编写高质量、高性能程序的关键。Python内置了多种功能强大的数据结构,让我们来一一探索它们。
四、常用数据结构:Python的“收纳盒”
Python内置的数据结构非常强大且易于使用,它们是日常编程和数据处理的基石。
4.1 列表(List):有序、可变的“万能容器”
-
特点:
-
有序(Ordered):元素有顺序,可以通过索引访问。
-
可变(Mutable):创建后可以修改(添加、删除、修改元素)。
-
异构(Heterogeneous):可以包含任意不同类型的元素(数字、字符串、甚至其他列表)。
-
-
创建:使用方括号
[]my_list = [1, "hello", 3.14, True] empty_list = [] -
索引(Indexing)与切片(Slicing):
-
索引:通过数字位置访问单个元素。索引从0开始。
-
正向索引:
my_list[0](第一个元素) -
反向索引:
my_list[-1](最后一个元素)
-
-
切片:提取列表的一部分。
-
语法:
list[start:end:step] -
start:开始索引(包含)。 -
end:结束索引(不包含)。 -
step:步长(默认为1)。
-
fruits = ["apple", "banana", "cherry", "date", "elderberry"] print(fruits[0]) # apple print(fruits[-2]) # date print(fruits[1:4]) # ['banana', 'cherry', 'date'] (从索引1到索引3) print(fruits[::2]) # ['apple', 'cherry', 'elderberry'] (步长为2) print(fruits[::-1]) # ['elderberry', 'date', 'cherry', 'banana', 'apple'] (反向列表) -
-
常用方法:
-
append(element):在列表末尾添加元素。 -
insert(index, element):在指定索引处插入元素。 -
pop([index]):移除并返回指定索引(默认为最后一个)的元素。 -
remove(value):移除列表中第一个匹配指定值的元素。 -
del list[index]/del list[start:end]:根据索引删除元素或切片。 -
sort():原地排序列表(修改原列表)。 -
sorted(list):返回一个新排序的列表,不改变原列表。 -
reverse():原地反转列表。 -
count(value):统计元素出现的次数。 -
index(value):返回元素第一次出现的索引。 -
copy():浅拷贝列表。 -
clear():清空列表。
my_list = [10, 20, 30] my_list.append(40) # [10, 20, 30, 40] my_list.insert(1, 15) # [10, 15, 20, 30, 40] my_list.pop() # 40,my_list变为 [10, 15, 20, 30] my_list.remove(15) # my_list变为 [10, 20, 30] my_list.sort() # 排序 -
-
嵌套列表:
- 列表可以包含其他列表,形成多维结构,常用于表示二维数组(矩阵)。
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] print(matrix[1][2]) # 6 -
比喻:列表就像一个可以无限扩展的、有编号的抽屉柜,你可以随时往里面添加、取出、修改任何抽屉里的东西。
4.2 元组(Tuple):有序、不可变的“固定容器”
-
特点:
-
有序(Ordered):元素有顺序,可以通过索引访问。
-
不可变(Immutable):一旦创建,其内容就不能被修改(不能添加、删除或替换元素)。
-
异构(Heterogeneous):可以包含任意不同类型的元素。
-
-
创建:使用圆括号
()。单个元素的元组需要在元素后加逗号(element,)。my_tuple = (1, "hello", 3.14) single_tuple = (1,) empty_tuple = () -
索引与切片:与列表类似。
-
用途:
-
常用于表示一组不可修改的数据,如坐标、日期(年月日)、函数返回的多个值。
-
由于不可变,元组可以作为字典的键(key)和集合的元素(element)(而列表不行)。
-
函数的多返回值实际上就是元组。
def get_coords(): return 10, 20 # 实际上返回的是一个元组 (10, 20) x, y = get_coords() -
-
比喻:元组就像一个有编号的、但一旦装好就不能再改变内容的密封盒子。
4.3 字典(Dictionary, Dict):键值对的“查找表”
-
特点:
-
无序(Unordered before Python 3.7)/有序(Ordered from Python 3.7+):在Python 3.7及之后版本,字典是插入有序的(保持插入的顺序),但在算法概念上仍被视为无序集合。
-
可变(Mutable):创建后可以修改。
-
键值对(Key-Value Pairs):以键(Key)和值(Value)的形式存储数据。每个键必须是唯一且不可变的(如字符串、数字、元组),值可以是任意类型。
-
高效查找:通过键可以非常快速地(平均时间复杂度O(1))查找、添加、删除对应的值。
-
-
创建:使用花括号
{}。person = {"name": "Alice", "age": 30, "city": "New York"} empty_dict = {} -
访问与修改:
-
通过键访问:
person["name"] -
通过键修改/添加:
person["age"] = 31,person["gender"] = "Female" -
使用
get(key, default_value)方法:如果键不存在,不会报错,而是返回None或指定的默认值。
print(person["name"]) # Alice print(person.get("country", "Unknown")) # Unknown (因为country键不存在) -
-
常用方法:
-
keys():返回所有键的视图。 -
values():返回所有值的视图。 -
items():返回所有键值对(元组)的视图。 -
pop(key, [default]):移除指定键的键值对,并返回其值。 -
update(other_dict):用另一个字典的键值对更新当前字典。 -
clear():清空字典。
for k, v in person.items(): print(f"{k}: {v}") -
-
比喻:字典就像一本有索引的电话簿或词典,你可以通过名字(键)快速找到电话号码(值),而不用从头到尾翻找。
4.4 集合(Set):无序、不重复的“去重容器”
-
特点:
-
无序(Unordered):元素没有顺序,不支持索引和切片。
-
可变(Mutable):可以添加和删除元素。
-
元素唯一(Unique):集合中的所有元素都必须是唯一的,不允许重复。
-
元素可哈希(Hashable):集合中的元素必须是不可变类型(如数字、字符串、元组),因为集合内部通过哈希表实现。
-
-
创建:使用花括号
{}(但空集合必须用set()创建,因为{}表示空字典)。my_set = {1, 2, 3, 2, 1} # 自动去重,my_set 变为 {1, 2, 3} empty_set = set() -
常用方法:
-
add(element):添加元素。 -
remove(element):移除元素,如果元素不存在会报错。 -
discard(element):移除元素,如果元素不存在不报错。 -
union(other_set)/|:并集。 -
intersection(other_set)/&:交集。 -
difference(other_set)/-:差集。 -
issubset():判断是否为子集。 -
issuperset():判断是否为超集。
s1 = {1, 2, 3} s2 = {3, 4, 5} s1.add(4) # {1, 2, 3, 4} print(s1.union(s2)) # {1, 2, 3, 4, 5} print(s1.intersection(s2)) # {3, 4} -
-
主要用途:
-
快速去重:将列表转换为集合再转回列表即可去重。
-
成员检测:判断元素是否存在于集合中(平均时间复杂度O(1))。
-
集合运算:交集、并集、差集等。
-
-
比喻:集合就像一个装满不同颜色珠子的袋子,你只能知道里面有哪些颜色的珠子,但不知道它们的顺序,而且相同的颜色不会放两颗。
4.5 字符串(String, str):字符的“序列”与“文本操作专家”
-
特点:
-
有序(Ordered):字符有顺序,可以通过索引访问。
-
不可变(Immutable):一旦创建,其内容就不能被修改。任何对字符串的修改操作都会生成一个新的字符串。
-
序列类型:支持索引和切片,与列表和元组类似。
-
-
创建:单引号、双引号、三引号。
my_string = "Hello Python!" -
索引与切片:与列表类似。
-
常用方法(由于不可变性,这些方法都返回新字符串):
-
len(string):获取字符串长度(这是内置函数,不是方法)。 -
upper()/lower():转大写/小写。 -
strip()/lstrip()/rstrip():移除字符串两端/左端/右端的空白字符。 -
split(separator):根据分隔符将字符串分割成列表。 -
join(iterable):将序列中的元素用指定字符串连接起来。 -
replace(old, new):替换子串。 -
find(substring)/index(substring):查找子串第一次出现的索引(index找不到会报错)。 -
startswith(prefix)/endswith(suffix):判断是否以指定前缀/后缀开头/结尾。 -
format()/f-string:格式化字符串,非常强大和常用。name = "Alice" age = 30 # format() 方法 print("My name is {} and I'm {} years old.".format(name, age)) # f-string (推荐,Python 3.6+) print(f"My name is {name} and I'm {age} years old.")
text = " Hello World! " cleaned_text = text.strip().lower().replace("world", "python") print(cleaned_text) # "hello python!" words = "apple,banana,cherry".split(',') print(words) # ['apple', 'banana', 'cherry'] joined_text = "-".join(words) print(joined_text) # "apple-banana-cherry" -
-
比喻:字符串就像一串珠子,每个珠子都有编号(索引),但一旦串好了就不能直接改变某个珠子的颜色,如果想改变,只能重新串一串新的。
同学们,Python内置的这些数据结构是你们进行数据处理、算法实现和程序设计的“基石”。熟练掌握它们,将大大提升你的编程效率和代码质量。
好的,同学们,我们继续Python编程基础的学习。上一节我们详细探讨了Python的各种内置数据结构,学会了如何有效地组织和存储数据。现在,我们要把目光转向如何更好地组织和复用代码——这就是函数式编程和面向对象编程的核心思想。
我们首先从函数式编程这个概念入手。函数是程序的基本构建块,它将一段完成特定任务的代码封装起来,可以被重复调用。理解函数,是编写模块化、可维护代码的关键。
五、函数式编程:代码的模块化与复用
函数式编程是一种编程范式,它将计算视为数学函数的求值,强调“纯函数”(无副作用)、不可变性。虽然Python不是纯粹的函数式语言,但它提供了很多函数式编程的特性和工具。
5.1 函数定义与调用:代码的“积木”
-
定义函数:
-
语法:以
def关键字开头,后跟函数名和圆括号内的参数列表,以冒号:结束。函数体需要缩进。 -
返回语句:
return关键字用于从函数中返回值。如果函数没有return语句,它会默认返回None。 -
Docstring(文档字符串):函数定义的第一行可以是一个多行字符串,用于描述函数的功能、参数、返回值等,方便查阅文档。
def greet(name): """ 这个函数用于向指定名字的人打招呼。 Args: name (str): 要打招呼的名字。 Returns: str: 拼接好的问候语。 """ message = f"Hello, {name}!" return message def add(x, y): result = x + y return result -
-
调用函数:
- 语法:
函数名(参数值)
greeting_msg = greet("Alice") print(greeting_msg) # Hello, Alice! sum_result = add(5, 3) print(sum_result) # 8 - 语法:
-
默认参数值:
-
可以在定义函数时为参数指定默认值。如果调用时没有为该参数提供值,则使用默认值。
-
注意:带默认值的参数必须放在不带默认值的参数之后。
def greet_default(name="Guest"): print(f"Hello, {name}!") greet_default() # Hello, Guest! greet_default("Bob") # Hello, Bob! -
-
关键字参数(Keyword Arguments):
- 调用函数时,可以通过
参数名=参数值的形式来传递参数,这可以提高代码可读性,并且不依赖参数的顺序。
def create_user(name, age, city): print(f"创建用户:姓名={name}, 年龄={age}, 城市={city}") create_user(name="Charlie", city="London", age=25) # 顺序可以打乱 - 调用函数时,可以通过
5.2 变量作用域:理解变量的“地盘”
变量的作用域决定了程序中哪些部分可以访问某个变量。
-
局部变量(Local Variables):
-
在函数内部定义的变量,只能在该函数内部访问。函数执行结束后,局部变量就会被销毁。
-
比喻:就像你只在一个房间里用过的东西,出了这个房间就不能再用了。
-
-
全局变量(Global Variables):
-
在函数外部定义的变量,可以在程序的任何地方(包括函数内部)访问。
-
比喻:就像你放在客厅里的东西,家里所有房间都能看到。
-
-
global关键字:-
如果需要在函数内部修改全局变量的值(而不是创建一个同名的局部变量),必须使用
global关键字声明。 -
老师提示:过度依赖全局变量会使代码难以理解和维护,不推荐。
global_var = 100 # 全局变量 def my_function(): local_var = 10 # 局部变量 print(f"函数内:全局变量={global_var}, 局部变量={local_var}") global global_var # 声明要修改全局变量 global_var = 200 # 修改全局变量 my_function() print(f"函数外:全局变量={global_var}") # 200 # print(local_var) # 报错:NameError: name 'local_var' is not defined -
-
nonlocal关键字:- 用于嵌套函数中,如果要在内部函数中修改其外层(但非全局)函数的变量,需要使用
nonlocal关键字。
- 用于嵌套函数中,如果要在内部函数中修改其外层(但非全局)函数的变量,需要使用
5.3 可变参数与关键字参数:函数的“灵活性”
Python函数支持非常灵活的参数传递方式。
-
可变位置参数(Positional Arguments with
*args):-
语法:在参数名前加一个星号
*。 -
作用:收集所有未命名的位置参数,将其封装为一个元组(tuple)。
-
示例:
def sum_all(*numbers): total = 0 for num in numbers: total += num return total print(sum_all(1, 2, 3)) # 6 print(sum_all(10, 20, 30, 40)) # 100
-
-
可变关键字参数(Keyword Arguments with
**kwargs):-
语法:在参数名前加两个星号
**。 -
作用:收集所有未被定义的关键字参数,将其封装为一个字典(dict)。
-
示例:
def display_info(**details): for key, value in details.items(): print(f"{key}: {value}") display_info(name="Bob", age=25, city="Paris") # 输出: # name: Bob # age: 25 # city: Paris
-
-
参数解包(Argument Unpacking):
-
在调用函数时,也可以使用
*和**来解包列表/元组和字典,将它们的元素作为参数传递给函数。 -
示例:
def my_function(a, b, c): print(a, b, c) my_list = [1, 2, 3] my_function(*my_list) # 解包列表,相当于my_function(1, 2, 3) my_dict = {'a': 10, 'b': 20, 'c': 30} my_function(**my_dict) # 解包字典,相当于my_function(a=10, b=20, c=30)
-
5.4 匿名函数(Lambda表达式):简洁的“一次性”函数
-
语法:
lambda arguments: expression -
作用:用于定义简单的、一次性的、无需命名的函数。Lambda函数体只能是一个表达式,并且会自动返回表达式的结果。
-
常见用途:作为高阶函数(如
map、filter、sorted的key参数)的参数,进行简单的逻辑处理。 -
示例:
# 普通函数 def add_normal(x, y): return x + y # 匿名函数 add_lambda = lambda x, y: x + y print(add_normal(2, 3)) # 5 print(add_lambda(2, 3)) # 5
5.5 内置高阶函数:操作函数的函数
高阶函数是指接收一个或多个函数作为参数,或者返回一个函数的函数。Python提供了几个常用的内置高阶函数。
-
map(function, iterable):-
作用:对可迭代对象中的每一个元素应用
function函数,返回一个map对象(生成器)。 -
示例:
numbers = [1, 2, 3, 4] # 将每个数字平方 squares = map(lambda x: x * x, numbers) print(list(squares)) # [1, 4, 9, 16]
-
-
filter(function, iterable):-
作用:过滤可迭代对象中不符合
function条件(返回False)的元素,返回一个filter对象(生成器)。 -
示例:
numbers = [1, 2, 3, 4, 5, 6] # 筛选出偶数 evens = filter(lambda x: x % 2 == 0, numbers) print(list(evens)) # [2, 4, 6]
-
-
reduce(function, iterable, [initializer]):-
注意:
reduce函数在Python 3中被移到functools模块中,需要from functools import reduce。 -
作用:对可迭代对象中的元素进行累积操作。
function必须接收两个参数,reduce会依次将函数应用于序列的元素,将结果传递给下一次调用。 -
示例:
from functools import reduce numbers = [1, 2, 3, 4] # 计算所有元素的和 sum_result = reduce(lambda x, y: x + y, numbers) print(sum_result) # 10 (1+2=3, 3+3=6, 6+4=10)
-
-
sorted(iterable, key=func, reverse=False):-
作用:对可迭代对象进行排序,返回一个新的排序后的列表。
-
key参数:一个函数,用于从每个元素中提取一个用于比较的键(key)。 -
示例:
students = [('Alice', 20, 95), ('Bob', 22, 88), ('Charlie', 19, 92)] # 按年龄排序 sorted_by_age = sorted(students, key=lambda s: s[1]) print(sorted_by_age) # [('Charlie', 19, 92), ('Alice', 20, 95), ('Bob', 22, 88)] # 按分数倒序排序 sorted_by_score_desc = sorted(students, key=lambda s: s[2], reverse=True) print(sorted_by_score_desc) # [('Alice', 20, 95), ('Charlie', 19, 92), ('Bob', 22, 88)]
-
5.6 装饰器(Decorator):为函数“穿上外衣”
-
概念:装饰器本质上是一个接收函数作为参数并返回一个新函数的高阶函数。它的作用是在不修改原函数代码的情况下,增加或扩展函数的功能。
-
语法糖:Python使用
@符号作为装饰器的语法糖,使其使用起来非常简洁。 -
常见应用场景:
-
日志记录:记录函数调用、参数和返回值。
-
权限校验:检查用户是否有权执行某个函数。
-
缓存:缓存函数执行结果,提高性能。
-
性能计时:计算函数执行时间。
-
事务处理:数据库事务的开启、提交、回滚。
-
-
示例:一个简单的日志装饰器
def log_decorator(func): # wrapper函数是装饰器返回的新函数,它会替代原函数被调用 def wrapper(*args, **kwargs): # *args和**kwargs用于接收原函数的所有位置和关键字参数 print(f"--- 调用函数: {func.__name__},参数: {args}, {kwargs} ---") result = func(*args, **kwargs) # 调用原函数 print(f"--- 函数 {func.__name__} 执行完毕,返回值: {result} ---") return result return wrapper # 使用@log_decorator装饰器来装饰add函数 @log_decorator def add(a, b): """计算两个数的和""" return a + b @log_decorator def multiply(x, y): """计算两个数的乘积""" return x * y # 调用被装饰的函数 res_add = add(10, 5) print(f"加法结果: {res_add}") res_mul = multiply(x=4, y=6) print(f"乘法结果: {res_mul}")- 当你调用
add(10, 5)时,实际执行的是wrapper函数,wrapper会打印日志,然后调用真正的add函数,再打印返回结果。
- 当你调用
函数式编程思想以及Python提供的这些特性,使得代码的组织更加灵活,逻辑更加清晰,是编写高质量Python代码的重要基础。
好的,同学们,我们继续Python编程基础的学习。上一节我们探讨了函数式编程以及Python的函数、高阶函数和装饰器,这些都是组织和复用代码的重要工具。现在,我们将进入Python的另一个核心编程范式——面向对象编程(Object-Oriented Programming, OOP)。
面向对象编程是一种强大的编程思想,它将数据和操作数据的方法封装在一起,形成“对象”,通过对象之间的交互来构建程序。这使得代码更具模块化、可维护性和可扩展性,尤其适合大型复杂项目的开发。
六、面向对象编程(OOP):程序的“模块化”与“模型化”
6.1 类与对象:蓝图与实体
-
类(Class):
-
含义:类是创建对象的蓝图或模板。它定义了对象的共同属性(数据)和行为(方法)。类本身并不占用内存空间,它只是一个抽象的定义。
-
比喻:就像一张汽车的设计图纸,它定义了汽车的颜色、轮子数量、发动机类型、能跑、能停等特性和功能。
-
-
对象(Object)/实例(Instance):
-
含义:对象是类的具体化实例。当你根据类创建了一个对象,就相当于有了一个具体的实体,它会占用内存空间。每个对象都有自己独立的属性值,但共享类定义的方法。
-
比喻:根据汽车设计图纸,实际生产出来的一辆辆具体的汽车。每辆车都有自己的颜色、车牌号,但它们都能跑、能停。
-
-
定义类:
-
语法:使用
class关键字。 -
__init__构造方法:-
这是一个特殊的**“魔法方法”(Magic Method),在创建类的新实例时自动调用**。
-
它用于初始化对象的属性。
-
self参数:self是__init__以及所有实例方法的第一个参数,它总是指向当前正在操作的对象实例本身。通过self,你可以在方法内部访问或修改对象的属性。
-
-
实例方法:
- 在类中定义的函数,用于描述对象的行为。它们也必须有一个
self作为第一个参数。
- 在类中定义的函数,用于描述对象的行为。它们也必须有一个
-
属性与方法:
-
属性(Attributes):对象的特征或数据,通常在
__init__方法中定义(如self.name)。 -
方法(Methods):对象的行为或操作,就是类中定义的函数。
-
-
-
示例:定义一个
Student类并创建对象class Student: # __init__ 方法是类的构造函数 def __init__(self, name, age, student_id): # self.name, self.age, self.student_id 都是实例属性 self.name = name self.age = age self.student_id = student_id self.courses = [] # 学生可能选修的课程列表 # 实例方法:描述学生可以做什么 def say_hello(self): print(f"Hello, I'm {self.name}, a student.") def enroll_course(self, course_name): self.courses.append(course_name) print(f"{self.name} 已选修课程:{course_name}") def get_age_in_years(self): return self.age # 创建Student类的对象(实例) # 创建对象时,会自动调用 __init__ 方法 s1 = Student('Tom', 18, '2023001') s2 = Student('Lucy', 20, '2023002') # 访问对象的属性 print(f"学生1的姓名:{s1.name}") # Tom print(f"学生2的学号:{s2.student_id}") # 2023002 # 调用对象的方法 s1.say_hello() # Hello, I'm Tom, a student. s2.enroll_course("Python编程") # Lucy 已选修课程:Python编程 s1.enroll_course("数据结构与算法") # Tom 已选修课程:数据结构与算法 print(f"Tom选修的课程:{s1.courses}") # ['数据结构与算法']
6.2 继承与多态:代码的“基因”与“变身”
面向对象编程的三个核心特性是封装、继承和多态。
-
继承(Inheritance):
-
含义:允许一个类(子类/派生类)继承另一个类(父类/基类)的属性和方法。子类可以重用父类的代码,并在此基础上添加新的功能或修改(重写)父类的行为。
-
优点:代码复用,减少重复代码,提高可维护性。
-
语法:在类名后括号中指定父类。
-
多继承:Python支持多继承(一个子类可以有多个父类)。
-
super()函数:- 作用:用于调用父类的方法,特别是在子类的构造函数中调用父类的构造函数来初始化父类部分的属性。
-
示例:
class Animal: def __init__(self, name): self.name = name print(f"{self.name} 诞生了!") def speak(self): print(f"{self.name} 发出声音...") class Dog(Animal): # Dog继承自Animal def __init__(self, name, breed): super().__init__(name) # 调用父类的构造函数 self.breed = breed print(f"我是 {self.breed} 的 {self.name}。") def speak(self): # 子类重写(Override)父类的方法 print(f"{self.name} 汪汪叫!") def fetch(self): print(f"{self.name} 正在叼东西。") class Cat(Animal): # Cat也继承自Animal def speak(self): print(f"{self.name} 喵喵叫!") my_dog = Dog("旺财", "金毛") my_dog.speak() # 旺财 汪汪叫! (调用了Dog类的speak方法) my_dog.fetch() # 旺财 正在叼东西。 my_cat = Cat("咪咪") my_cat.speak() # 咪咪 喵喵叫! (调用了Cat类的speak方法)
-
-
多态(Polymorphism):
-
含义:指不同类的对象对同一个方法调用表现出不同的行为。这依赖于继承和方法重写。
-
优点:增加了代码的灵活性和可扩展性,无需知道对象的具体类型,只需要知道它有某个方法即可调用。
-
比喻:一个“会说话”的按钮,在Word里点击是“保存文档”,在浏览器里点击是“提交表单”。虽然都是“点击”这个动作,但具体表现不同。
-
示例:
def animal_sound(animal): # 定义一个函数,接收一个Animal对象或其子类对象 animal.speak() # 调用对象的speak方法 animal_sound(my_dog) # 旺财 汪汪叫! animal_sound(my_cat) # 咪咪 喵喵叫!这里
animal_sound函数并不知道animal是Dog还是Cat,但它知道animal有一个speak方法,并且会根据实际对象的类型调用其对应的方法,这就是多态。
-
6.3 封装与私有属性:保护对象的“秘密”
-
封装(Encapsulation):
-
含义:将数据(属性)和操作数据的方法(行为)捆绑在一起,形成一个独立的单元(对象)。并对外部隐藏对象的内部实现细节,只暴露有限的接口进行交互。
-
优点:
-
提高安全性:防止外部代码随意修改对象的内部状态。
-
降低耦合度:改变内部实现不影响外部接口。
-
提高可维护性:模块化设计,问题更容易定位。
-
-
比喻:你使用手机,只需要知道按哪个按钮可以拍照,而不需要知道手机内部的摄像头是如何工作的。
-
-
私有属性(Private Attributes):
-
Python中没有严格意义上的“私有”访问控制符(如Java的
private)。它通过命名约定来示意。 -
以单个下划线
_开头的属性:_attribute。这是一种**“保护属性”**的约定,表示这个属性是内部使用的,外部代码应该避免直接访问,但技术上仍然可以访问。 -
以双下划线
__开头的属性:__attribute。这会触发Python的**名称修饰(Name Mangling)**机制,将属性名转换为_ClassName__attribute的形式,从而在一定程度上“隐藏”了属性,避免与子类同名属性冲突。但这也不是真正的私有,依然可以通过_ClassName__attribute的形式访问。 -
推荐做法:通过**公共方法(Getter/Setter)**来对外暴露或修改属性,进行必要的校验和逻辑控制。
-
示例:
class BankAccount: def __init__(self, owner, balance): self.__owner = owner # 伪私有属性 self.__balance = balance def get_balance(self): # Getter方法 return self.__balance def deposit(self, amount): # Setter方法,带逻辑校验 if amount > 0: self.__balance += amount print(f"存入 {amount},当前余额:{self.__balance}") else: print("存入金额必须大于0。") # 尝试直接访问伪私有属性会报错或提示 # print(account.__balance) # AttributeError: 'BankAccount' object has no attribute '__balance' # 实际被转换为 _BankAccount__balance # print(account._BankAccount__balance) # 这样可以访问,但不推荐
-
6.4 类变量与实例变量:共享与独有
-
实例变量(Instance Variables):
-
定义:在
__init__方法中,通过self前缀定义的变量(如self.name)。 -
特点:每个对象(实例)都有自己独立的一份副本。它们的值可以不同,互不影响。
-
比喻:每个学生都有自己的姓名和年龄。
-
-
类变量(Class Variables):
-
定义:在类内部、方法外部定义的变量。
-
特点:所有该类的对象共享同一份副本。它们可以通过类名或实例名访问(但不推荐通过实例名修改,否则会创建同名实例变量)。
-
用途:存储所有实例共享的数据,如常量、计数器等。
-
示例:
class Building: total_buildings = 0 # 类变量,所有Building实例共享 def __init__(self, name): self.name = name # 实例变量 Building.total_buildings += 1 # 通过类名修改类变量 def get_total_buildings(): # 类方法或静态方法访问类变量 return Building.total_buildings b1 = Building("Tower A") b2 = Building("Tower B") print(f"总建筑数量:{Building.total_buildings}") # 2 print(f"通过实例访问:{b1.total_buildings}") # 2 (但不推荐这样修改,因为会创建实例变量)
-
6.5 魔法方法(Magic Methods)与特殊属性:Python的“魔术”
-
魔法方法(Magic Methods):
-
含义:以双下划线开头和结尾(如
__init__,__str__,__len__)的特殊方法。它们允许你定义类的行为,使其能够响应内置操作符或函数。 -
也称为“Dunder Methods”(Double Underscore)。
-
用途:实现运算符重载、定制对象的字符串表示、使对象可迭代、可比较等。
-
-
常用魔法方法:
-
__str__(self):用于返回对象的“用户友好”的字符串表示(当使用print()或str()时调用)。 -
__repr__(self):用于返回对象的“开发人员友好”的字符串表示(当在交互式解释器中直接输入对象名或repr()时调用)。 -
__len__(self):当使用len()函数时调用,定义对象的长度。 -
__getitem__(self, key):当使用索引obj[key]访问对象时调用。 -
__setitem__(self, key, value):当使用obj[key] = value设置对象时调用。 -
__add__(self, other):实现+运算符的行为。 -
__eq__(self, other):实现==运算符的行为。
-
-
示例:定制对象的字符串表示和比较行为
class Point: def __init__(self, x, y): self.x = x self.y = y def __str__(self): # 用户友好表示 return f"Point({self.x}, {self.y})" def __repr__(self): # 开发人员表示 return f"Point(x={self.x}, y={self.y})" def __eq__(self, other): # 重载 == 运算符 if isinstance(other, Point): # 检查类型,防止与其他类型比较 return self.x == other.x and self.y == other.y return NotImplemented # 否则返回 NotImplemented def __add__(self, other): # 重载 + 运算符 if isinstance(other, Point): return Point(self.x + other.x, self.y + other.y) return NotImplemented p1 = Point(1, 2) p2 = Point(3, 4) p3 = Point(1, 2) print(p1) # Point(1, 2) (调用 __str__) print([p1, p2]) # [Point(x=1, y=2), Point(x=3, y=4)] (列表调用元素的 __repr__) print(p1 == p3) # True (调用 __eq__) print(p1 == p2) # False print(p1 + p2) # Point(4, 6) (调用 __add__)
同学们,面向对象编程是软件工程中非常重要的思想。掌握类、对象、继承、多态、封装和魔法方法,你就能以一种更抽象、更系统的方式来设计和构建你的程序,尤其是在开发大型、复杂的应用时,OOP的优势会体现得淋漓尽致。
好的,同学们,我们继续Python编程基础的学习。上一节我们全面探讨了面向对象编程(OOP),学习了如何用类和对象来构建模块化、可复用、可扩展的代码。现在,我们要把目光转向程序开发中一个不可避免但又至关重要的话题——异常处理。
在程序运行过程中,难免会遇到各种意想不到的错误,比如除数为零、文件不存在、网络中断、类型不匹配等。如果不对这些错误进行妥善处理,程序很可能会直接崩溃,导致糟糕的用户体验甚至数据丢失。Python提供了优雅的异常处理机制,让我们能够预测、捕获并处理这些运行时错误,使程序更加健壮。
七、异常处理:让你的程序更健壮
7.1 try-except结构:捕获与处理错误
Python使用try-except语句块来捕获和处理可能发生的异常。
-
基本语法:
try: # 尝试执行的代码块 (可能引发异常的代码放这里) # 例如:文件操作,网络请求,类型转换,除法等 except ExceptionType as e: # 如果try块中发生了 ExceptionType 类型的异常,则执行这里的代码 # e 是一个变量,用于捕获异常对象,可以打印异常信息 else: # (可选) 如果try块中的代码没有引发任何异常,则执行这里的代码 finally: # (可选) 无论try块中是否发生异常,也无论是否被except捕获,finally块中的代码都一定会执行 # 通常用于资源清理,如关闭文件、数据库连接等 -
示例:捕获除零错误和文件不存在错误
# 示例1:捕获特定类型的异常 try: num1 = 10 num2 = 0 result = num1 / num2 # 这里会引发 ZeroDivisionError print(f"计算结果: {result}") except ZeroDivisionError as e: print(f"发生除零错误:{e}") print("请检查你的除数,确保不为零。") print("程序继续执行...") # 即使发生异常,程序也不会崩溃 print("-" * 30) # 示例2:捕获多个不同类型的异常 try: data = int("abc") # 尝试将字符串转为整数,会引发 ValueError # with open("non_existent_file.txt", "r") as f: # 如果文件不存在,会引发 FileNotFoundError # content = f.read() # print(content) except ValueError as e: print(f"发生值错误:{e}") print("请确保输入是有效的数字。") except FileNotFoundError as e: print(f"文件未找到错误:{e}") print("请检查文件路径是否正确。") except Exception as e: # 捕获所有其他未知类型的异常,作为最后的“兜底” print(f"发生了一个未知的错误:{e}") else: print("try块中的代码没有发生任何异常。") finally: print("无论是否发生异常,这部分代码都会被执行,通常用于清理资源。") print("-" * 30) # 示例3:一个结合了 else 和 finally 的完整例子 file_name = "my_data.txt" try: # 尝试写入文件 with open(file_name, "w") as f: f.write("这是我写入的数据。\n") f.write("下一行数据。\n") print(f"文件 '{file_name}' 写入成功。") except IOError as e: # 捕获I/O错误,如权限不足 print(f"写入文件时发生I/O错误:{e}") else: # 如果try块执行成功,尝试读取文件 try: with open(file_name, "r") as f: content = f.read() print(f"文件 '{file_name}' 内容:\n{content}") except FileNotFoundError: # 理论上不会发生,因为刚刚才写入 print("读取时文件未找到!") except Exception as e: print(f"读取文件时发生未知错误:{e}") finally: print("文件操作尝试结束。") # 无论成功或失败,都会执行 # 这里可以放置关闭数据库连接、释放网络资源等代码
7.2 自定义异常:量身定制错误类型
Python允许我们定义自己的异常类型,这在大型项目中非常有用,可以使错误信息更具语义化,方便调试和处理。
-
语法:自定义异常类通常继承自内置的
Exception类(或其子类)。 -
示例:
# 定义一个自定义异常类 class InvalidInputError(Exception): """自定义异常:无效的输入。""" def __init__(self, message="输入数据不符合要求"): self.message = message super().__init__(self.message) # 调用父类的构造函数 def process_data(data): if not isinstance(data, (int, float)): raise InvalidInputError("输入必须是数字类型。") if data < 0: raise InvalidInputError("输入数据不能为负数。") print(f"处理数据:{data}") return data * 2 try: process_data(10) process_data(-5) # 会引发 InvalidInputError # process_data("abc") # 会引发 InvalidInputError except InvalidInputError as e: print(f"自定义异常捕获:{e}") except Exception as e: print(f"捕获到其他意外错误:{e}") -
raise关键字:用于主动抛出一个异常。当程序检测到不符合预期的条件时,可以使用raise来中断当前执行流并抛出异常。
7.3 常见异常类型:知己知彼,百战不殆
了解Python内置的一些常见异常类型,有助于我们更精确地捕获和处理错误。
-
ValueError:当函数或操作接收到有效类型但值不合适时引发。- 例子:
int("abc"),math.sqrt(-1)。
- 例子:
-
TypeError:当操作或函数应用于不适当类型的对象时引发。- 例子:
"hello" + 10,len(123)。
- 例子:
-
KeyError:当字典中使用的键(key)不存在时引发。- 例子:
my_dict['non_existent_key']。
- 例子:
-
IndexError:当序列(列表、元组、字符串)的索引超出范围时引发。- 例子:
my_list[10](列表只有5个元素)。
- 例子:
-
FileNotFoundError:尝试打开不存在的文件时引发。- 例子:
open("non_existent.txt", "r")。
- 例子:
-
IOError:通用的I/O操作错误,如文件读写权限问题、磁盘空间不足等。 -
ZeroDivisionError:当除法运算的除数为零时引发。- 例子:
10 / 0。
- 例子:
-
AssertionError:当assert语句的条件为False时引发,通常用于调试或前置条件检查。- 例子:
assert x > 0, "x必须大于0"。
- 例子:
-
NameError:当尝试使用一个未定义的变量或函数名时引发。- 例子:
print(undeclared_var)。
- 例子:
-
AttributeError:当尝试访问对象的一个不存在的属性或方法时引发。- 例子:
"hello".upper_case()(upper_case不是字符串的方法)。
- 例子:
老师提示:捕获异常时,应尽可能捕获具体的异常类型,而不是简单地使用except Exception as e:来捕获所有异常。捕获具体异常能让你更有针对性地处理问题,避免“吞掉”本应暴露的程序逻辑错误。
异常处理是编写健壮、可靠程序的重要组成部分。通过有效地使用try-except-else-finally结构,你可以让你的程序更好地应对各种运行时挑战,提供更平滑的用户体验。
好的,同学们,我们继续Python编程基础的学习。上一节我们全面探讨了异常处理,学会了如何让程序在遇到错误时依然健壮运行。现在,我们要把目光转向Python的“生态系统”——模块与包管理,以及Python强大且丰富的标准库。
当程序变得越来越复杂,代码量越来越大时,我们不可能把所有代码都写在一个文件里。这时,就需要将代码进行模块化,以便于组织、管理、复用和协作。Python提供了完善的模块(Module)和包(Package)机制来实现这一目标。
八、模块与包管理:代码的“组织者”与“共享者”
8.1 模块的引入与使用:Python文件的复用
-
什么是模块(Module):
-
在Python中,一个
.py文件就是一个模块。模块可以包含函数、类、变量、以及可执行的代码。 -
作用:模块化使代码结构清晰,便于管理和复用。你可以将完成特定功能的代码封装在一个模块中,然后在其他地方引用它。
-
比喻:模块就像一本书的一个章节,或者一个功能专一的工具箱。
-
-
模块的引入(Import):
-
import 模块名:导入整个模块。在使用模块中的内容时,需要加上模块名前缀。# math.py 是Python内置的数学模块 import math print(math.pi) # 访问模块中的变量 print(math.sqrt(25)) # 访问模块中的函数 -
import 模块名 as 别名:导入整个模块并给它取一个短别名,方便使用。import math as m print(m.pi) -
from 模块 import 名称:从模块中导入特定的函数、类或变量。导入后可以直接使用,无需模块名前缀。from math import pi, sqrt print(pi) print(sqrt(36)) -
from 模块 import *:导入模块中的所有公共名称。- 老师提示:不推荐使用,因为它会污染当前命名空间,可能导致名称冲突和代码可读性下降,难以追溯变量来源。
-
-
常用内置模块:
-
Python自带了大量功能强大的模块,这些模块无需额外安装,可以直接
import使用,被称为Python标准库(Python Standard Library)。 -
os:操作系统接口模块,用于文件和目录操作、进程管理、环境变量等。 -
sys:系统相关参数和函数,如命令行参数、退出程序等。 -
datetime:日期和时间处理。 -
random:生成随机数。 -
json:处理JSON数据(序列化/反序列化)。 -
re:正则表达式模块。 -
math:数学运算,如三角函数、对数、平方根等。 -
logging:灵活的日志记录系统。 -
collections:提供一些额外的数据结构,如deque(双端队列)、Counter(计数器)、defaultdict等。 -
urllib/requests(第三方):网络请求(requests是非内置但非常常用的第三方库)。 -
shutil:文件和目录的高级操作,如复制、移动、删除目录树。
-
8.2 第三方包管理工具:Python的“应用商店”
Python的强大之处在于其极其庞大和活跃的第三方包(Third-party Packages)生态系统。这些包由全球开发者贡献,覆盖了各种领域。
-
pip(Pip Installs Packages):-
含义:Python官方推荐的包管理工具,用于安装、卸载、管理Python第三方库。它通常随着Python安装程序一起安装。
-
常用命令:
-
pip install 包名:安装一个包,如pip install requests。 -
pip install 包名==版本号:安装指定版本的包,如pip install Django==4.2.0。 -
pip list:列出所有已安装的包。 -
pip show 包名:显示包的详细信息。 -
pip uninstall 包名:卸载一个包。 -
pip freeze > requirements.txt:将当前环境的所有包及其版本导出到requirements.txt文件,方便项目依赖管理。 -
pip install -r requirements.txt:根据requirements.txt文件安装所有依赖包。
-
-
-
虚拟环境(Virtual Environments):
-
目的:在开发多个Python项目时,不同的项目可能需要不同版本的相同库,或者需要隔离各自的依赖。
-
作用:虚拟环境为每个项目创建一个独立、隔离的Python运行环境,在这个环境中安装的包不会影响到系统全局的Python环境,也不会影响到其他项目的虚拟环境。
-
常用工具:
-
venv:Python 3.3+ 自带的虚拟环境工具。-
创建:
python -m venv my_project_env -
激活:
source my_project_env/bin/activate(Linux/macOS) 或my_project_env\Scripts\activate(Windows) -
退出:
deactivate
-
-
virtualenv:更早、功能更丰富的第三方虚拟环境工具。 -
conda:Anaconda发行版自带的包和环境管理工具,功能比pip/venv更强大,可管理非Python依赖。
-
-
比喻:你可以在一台电脑上同时拥有多个独立的“Python沙盒”,每个沙盒都只安装了你当前项目所需的工具,互不干扰。
-
8.3 自定义模块和包:构建你自己的代码库
-
自定义模块:
-
你编写的任何
.py文件,都可以被其他Python文件当作模块导入和使用。 -
示例:
-
创建
my_utils.py:# my_utils.py def greet(name): return f"Hello, {name}!" PI = 3.14159 -
在另一个文件
main.py中导入:# main.py import my_utils print(my_utils.greet("World")) print(my_utils.PI)
-
-
-
什么是包(Package):
-
包是一种组织模块的方式,它是一个包含
__init__.py文件的目录。 -
作用:包可以包含多个模块,也可以包含子包,形成层次化的结构,便于组织大型项目。
-
__init__.py文件:它是一个空文件(或包含一些初始化代码),用于标识当前目录是一个Python包。 -
示例:
my_project/ ├── main.py └── my_package/ ├── __init__.py # 标识my_package是一个包 ├── module_a.py └── sub_package/ └── __init__.py └── module_b.py -
在
main.py中导入module_b:from my_package.sub_package import module_b
-
-
相对导入与绝对导入:
-
绝对导入:
from package.subpackage import module(推荐,清晰明确) -
相对导入:
from . import module_a或from .. import parent_module(在包内部使用,不推荐在包外使用)
-
九、Python标准库高频应用:Python自带的“电池”
Python标准库是Python安装时自带的一套模块,功能强大且稳定,几乎涵盖了所有日常编程任务。
9.1 文件与目录操作:处理“硬盘上的数据”
-
内置
open()函数:用于打开文件,并返回文件对象。配合with语句可以确保文件自动关闭。# 写入文件 file_path = 'example.txt' with open(file_path, 'w', encoding='utf-8') as f: # 'w' 写入模式,如果文件存在会覆盖 f.write("Hello, Python File I/O!\n") f.write("This is the second line.\n") print(f"文件 '{file_path}' 写入成功。") # 读取文件 with open(file_path, 'r', encoding='utf-8') as f: # 'r' 读取模式 content = f.read() # 读取所有内容 print("\n--- 文件内容 ---") print(content) # 逐行读取文件 print("\n--- 逐行读取 ---") with open(file_path, 'r', encoding='utf-8') as f: for line in f: print(line.strip()) # strip() 移除行尾的换行符 -
os模块:与操作系统交互,进行文件和目录的创建、删除、重命名、路径操作等。import os # 获取当前工作目录 print(f"当前工作目录: {os.getcwd()}") # 创建目录 os.mkdir("new_folder") # 判断路径是否存在 print(f"new_folder是否存在: {os.path.exists('new_folder')}") # 列出目录内容 print(f"new_folder内容: {os.listdir('new_folder')}") # 删除空目录 os.rmdir("new_folder") -
shutil模块:高级文件操作,如复制、移动整个目录树。import shutil # 复制文件 # shutil.copy("source.txt", "destination.txt") # 复制目录树 # shutil.copytree("source_dir", "destination_dir") -
pathlib模块(推荐):面向对象的路径操作,更直观和安全。from pathlib import Path p = Path('my_document.txt') print(f"文件名: {p.name}") print(f"文件后缀: {p.suffix}") print(f"父目录: {p.parent}") new_p = Path('new_dir') / 'another_file.log' # 拼接路径 new_p.touch() # 创建文件
9.2 日期与时间:处理“时间戳”
-
datetime模块:处理日期、时间、时间间隔等。import datetime # 获取当前日期和时间 now = datetime.datetime.now() print(f"当前时间: {now}") # 格式化日期时间 print(f"格式化: {now.strftime('%Y-%m-%d %H:%M:%S')}") # YYYY-MM-DD HH:MM:SS print(f"短格式: {now.strftime('%y%m%d')}") # 231110 # 解析字符串为日期时间对象 date_str = "2023-01-15 10:30:00" parsed_date = datetime.datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S') print(f"解析后的日期: {parsed_date}") # 计算时间差 one_day = datetime.timedelta(days=1) tomorrow = now + one_day print(f"明天: {tomorrow.strftime('%Y-%m-%d')}")
9.3 网络与HTTP:与Web世界交互
-
urllib.request模块:Python内置的HTTP请求库。import urllib.request try: response = urllib.request.urlopen('https://www.example.com') html_content = response.read().decode('utf-8') print(f"网页内容长度: {len(html_content)} 字节") except Exception as e: print(f"访问网页失败: {e}") -
requests模块(第三方库,非常推荐):比urllib更简单、更强大的HTTP客户端库。几乎所有Python Web开发和爬虫都会用它。# 需要先安装:pip install requests import requests try: response = requests.get('https://api.github.com/users/octocat') response.raise_for_status() # 如果请求失败 (非2xx状态码) 则抛出异常 user_data = response.json() # 将响应体解析为JSON print(f"GitHub 用户名: {user_data['login']}, ID: {user_data['id']}") except requests.exceptions.RequestException as e: print(f"请求失败: {e}")
9.4 JSON解析:数据交换的桥梁
-
json模块:用于处理JSON(JavaScript Object Notation)数据,JSON是Web服务中常用的数据交换格式。-
序列化(Serialization):将Python对象(字典、列表等)转换为JSON字符串。
-
反序列化(Deserialization):将JSON字符串转换为Python对象。
import json # Python字典 data = { 'name': 'Charlie', 'age': 28, 'isStudent': False, 'hobbies': ['reading', 'coding'], 'address': None } # 将Python字典序列化为JSON字符串 json_string = json.dumps(data, indent=4, ensure_ascii=False) # indent=4 美化输出,ensure_ascii=False 支持中文 print("\n--- JSON 字符串 ---") print(json_string) # 将JSON字符串反序列化为Python字典 parsed_data = json.loads(json_string) print("\n--- 解析后的 Python 字典 ---") print(parsed_data['name']) -
9.5 系统参数与环境:获取程序运行信息
-
sys模块:提供对解释器使用或维护的变量以及与解释器强交互的函数的访问。import sys # 获取命令行参数(第一个参数是脚本名) print(f"命令行参数: {sys.argv}") # 退出程序 # sys.exit(0) # 0表示正常退出 -
os模块:再次提到os,它也可以获取环境变量、执行系统命令等。import os # 获取环境变量 print(f"PATH 环境变量: {os.environ.get('PATH')}")
通过本节的学习,大家应该对Python的模块化机制以及其强大的标准库有了初步的认识。这些模块和库将极大地提升你的开发效率。
好的,同学们,我们继续Python编程基础的学习!至此,我们已经系统学习了Python的语法、数据结构、流程控制、函数、面向对象编程、异常处理以及模块与包管理。现在,是时候将这些知识串联起来,通过一个实践项目来巩固所学,并为后续课程打下坚实的基础。
十、实践项目:命令行工具开发
在这个项目中,我们将开发一个实用的命令行工具。通过这个工具,你将体验到Python在文件操作、参数解析和逻辑控制方面的强大能力。
10.1 项目目标
-
目标:开发一个命令行下的批量文件重命名工具。这个工具能够:
-
接收命令行参数,指定要处理的目录。
-
支持为重命名后的文件添加自定义前缀和后缀。
-
支持根据文件扩展名进行过滤(只重命名特定类型的文件)。
-
能够为重命名后的文件添加序号。
-
执行重命名操作后,输出详细的重命名报告。
-
10.2 实现思路:从需求到代码
-
解析命令行参数:
-
Python提供
sys.argv来获取原始命令行参数,但更推荐使用argparse模块。argparse是一个功能强大的库,可以方便地定义、解析命令行参数,并自动生成帮助信息。 -
我们需要定义的参数:
-
--dir:指定要操作的目录(默认当前目录)。 -
--prefix:指定新文件名的前缀(默认空)。 -
--suffix:指定新文件名的后缀(默认空)。 -
--ext:指定要过滤的文件扩展名(默认所有文件)。 -
--start-num:序号的起始数字(默认1)。 -
--dry-run:可选,模拟运行,只显示将要进行的重命名操作,不实际修改文件。
-
-
-
遍历目录,筛选目标文件:
-
使用
os.listdir()或pathlib.Path.iterdir()遍历指定目录下的所有文件和子目录。 -
使用
os.path.isfile()或Path.is_file()判断是否为文件。 -
根据
--ext参数进行文件扩展名过滤。
-
-
生成新文件名并重命名:
-
根据前缀、序号、后缀、原始扩展名来构建新文件名。
-
使用
os.rename()或Path.rename()进行文件重命名。
-
-
输出重命名报告:
-
打印旧文件名和新文件名之间的映射关系。
-
如果启用
--dry-run,只打印报告而不实际操作。
-
10.3 示例代码:一个简单的批量重命名工具
import os
import argparse
from pathlib import Path # 推荐使用pathlib进行路径操作
def rename_files(directory, prefix, suffix, ext_filter, start_num, dry_run):
"""
批量重命名指定目录下的文件。
Args:
directory (str): 要操作的目录路径。
prefix (str): 新文件名的前缀。
suffix (str): 新文件名的后缀。
ext_filter (str): 要过滤的文件扩展名(例如 '.txt', '.jpg'),空字符串表示不过滤。
start_num (int): 序号的起始数字。
dry_run (bool): 如果为True,则只打印将要执行的操作,不实际重命名文件。
"""
path = Path(directory)
if not path.is_dir():
print(f"错误:'{directory}' 不是一个有效的目录。")
return
print(f"\n--- 正在处理目录: {path.absolute()} ---")
if dry_run:
print("--- 模拟运行模式 (文件不会实际修改) ---")
files_to_rename = []
# 遍历目录,收集符合条件的文件
for item in path.iterdir():
if item.is_file():
# 如果指定了扩展名过滤,则进行检查
if ext_filter and item.suffix != ext_filter:
continue
files_to_rename.append(item)
# 对文件列表进行排序,确保序号的顺序一致(按文件名自然排序)
files_to_rename.sort()
if not files_to_rename:
print("没有找到符合条件的文件。")
return
print(f"找到 {len(files_to_rename)} 个文件符合条件。")
print("\n--- 重命名报告 ---")
for index, old_path in enumerate(files_to_rename, start=start_num):
old_name = old_path.name
old_stem = old_path.stem # 文件名(不含扩展名)
old_suffix = old_path.suffix # 扩展名
# 构建新文件名
# 例子:file_001_new.txt
new_name = f"{prefix}{index:03d}{suffix}{old_suffix}" # index:03d 表示数字占3位,不足3位补0
new_path = old_path.with_name(new_name) # 使用pathlib的方式生成新路径
print(f"'{old_name}' -> '{new_name}'")
if not dry_run:
try:
old_path.rename(new_path) # 执行重命名
except OSError as e:
print(f"错误:无法重命名 '{old_name}' 到 '{new_name}' - {e}")
print("\n--- 操作完成 ---")
if dry_run:
print("提示:在模拟运行模式下,文件未被修改。如需实际重命名,请移除 --dry-run 参数。")
if __name__ == '__main__':
# 1. 创建ArgumentParser对象
parser = argparse.ArgumentParser(
description="一个命令行文件批量重命名工具。",
formatter_class=argparse.RawTextHelpFormatter # 保持帮助信息的格式
)
# 2. 定义命令行参数
parser.add_argument('--dir', type=str, default='.',
help='指定要操作的目录 (默认: 当前目录).')
parser.add_argument('--prefix', type=str, default='',
help='指定新文件名的前缀 (默认: 无).')
parser.add_argument('--suffix', type=str, default='',
help='指定新文件名的后缀 (默认: 无). 例如 "_processed".')
parser.add_argument('--ext', type=str, default='',
help='指定要过滤的文件扩展名 (例如 ".txt", ".jpg").\n空字符串表示处理所有文件 (默认: 无过滤).')
parser.add_argument('--start-num', type=int, default=1,
help='序号的起始数字 (默认: 1).')
parser.add_argument('--dry-run', action='store_true',
help='模拟运行模式,只显示重命名计划,不实际修改文件.')
# 3. 解析命令行参数
args = parser.parse_args()
# 4. 调用核心重命名函数
rename_files(
directory=args.dir,
prefix=args.prefix,
suffix=args.suffix,
ext_filter=args.ext,
start_num=args.start_num,
dry_run=args.dry_run
)
如何运行和测试这个工具:
-
将上述代码保存为
rename_tool.py。 -
创建测试目录和文件:
-
在
rename_tool.py同级目录下创建test_files目录。 -
在
test_files中创建一些文件,例如:document.txt photo.jpg report_final.txt image.png temp.txt
-
-
运行示例(模拟运行):
-
python rename_tool.py --dir test_files --prefix "new_doc_" --ext ".txt" --dry-run -
你将看到程序打印出计划的重命名操作,但文件不会改变。
-
-
运行示例(实际重命名):
-
python rename_tool.py --dir test_files --prefix "my_photo_" --ext ".jpg" -
你会发现
photo.jpg变成了my_photo_001.jpg。
-
-
查看帮助:
python rename_tool.py --help,可以看到argparse自动生成的帮助信息。
通过这个项目,你将实际运用到文件I/O、字符串格式化、列表操作、条件判断、循环以及命令行参数解析等多个知识点。
十一、与后续课程的紧密衔接:Python的“中枢”地位
Python在全栈开发中扮演着“中枢”的角色,它将你前面学到的底层知识与后面要学的高级应用紧密连接起来。
-
数据结构:
-
Python的列表、字典、集合等内置数据结构,就是我们后面专门学习“数据结构”的天然载体。你会发现许多抽象的数据结构(如链表、树、图)都可以用Python内置的数据类型或类来高效实现。
-
学习了Python,你就有了实现和验证数据结构和算法的绝佳平台。
-
-
算法与编程能力提升:
-
掌握Python语法后,你就可以专注于算法本身的逻辑和设计,而不是纠结于语言细节。
-
解决LeetCode、牛客网等平台上的算法题,Python将是你的首选语言,因为它能够让你快速验证算法思路。
-
-
Web/全栈开发:
- 本节Python是后续学习后端框架(如Flask/Django/FastAPI)的基础。你将用Python来编写HTTP API、处理数据库交互、实现业务逻辑,最终与前端(Vue/React等)集成,构建完整的Web应用。
-
自动化运维与数据处理:
-
Python是自动化运维领域最常用的语言之一,比Shell脚本更强大、更灵活。你可以用它来编写复杂的系统管理脚本、部署脚本、监控脚本。
-
在数据处理和网络爬虫领域,Python更是无可争议的主力。
-
-
云原生与AI:
-
Python在人工智能和云计算领域拥有极其成熟和庞大的生态系统(TensorFlow, PyTorch, NumPy, Pandas等)。后续的AI相关课程将大量依赖Python。
-
云服务提供商也为Python提供了丰富的SDK,便于你通过代码操作云资源。
-
十二、学习建议与资源拓展:持续精进
-
动手练习,多写小脚本:不要只看不练。从小问题入手,编写Python脚本来解决实际生活或工作中的痛点,例如:
-
自动整理下载文件夹。
-
批量处理图片。
-
发送定时邮件提醒。
-
生成简单的报表。
-
-
参与项目,刷题实践:
-
尝试解决LeetCode、牛客网等在线平台上的编程题目。
-
参与一些简单的开源项目,提交你的代码。
-
-
阅读官方文档:Python官方文档是最好的学习资源,它详细、准确、权威。
-
推荐书籍:
-
《流畅的Python》(Fluent Python):适合有一定基础,想深入理解Python高级特性的读者。
-
《Python编程:从入门到实践》(Python Crash Course):非常适合初学者,项目驱动。
-
《Python核心编程》(Core Python Programming):全面而深入,适合作为参考书。
-
-
在线教程与社区:
-
廖雪峰的Python教程。
-
知乎、CSDN、Stack Overflow等技术社区,遇到问题多搜索,多提问。
-
十三、课后练习与思考:挑战自己,巩固所学
-
回文判断函数:
- 编写一个Python函数,接收一个字符串作为输入。判断该字符串是否是回文字符串(正读反读都一样,例如“上海自来水来自海上”)。忽略大小写和非字母数字字符。
-
小型通讯录管理系统:
-
设计一个简单的Python程序,实现一个基于字典的通讯录管理系统。支持以下功能:
-
添加联系人(姓名、电话、邮箱)
-
删除联系人(按姓名)
-
查找联系人(按姓名或电话模糊查找)
-
显示所有联系人
-
数据持久化(将通讯录保存到文件,程序启动时加载)。
-
-
-
文本文件单词计数与排序:
-
编写一个Python脚本,读取一个文本文件。统计文件中每个单词出现的次数,并按照出现频率从高到低排序输出。
-
提示:使用字典进行计数,使用字符串的
split()方法,处理标点符号和大小写。
-
-
用装饰器统计函数运行时间:
-
编写一个Python装饰器,用于计算并打印被装饰函数的执行时间。然后用它装饰你之前编写的任何一个函数。
-
提示:使用
time模块的time.time()或time.perf_counter()函数。
-
-
思考:
-
为何Python语言如此适合初学者入门,又能在数据科学、人工智能和全栈开发等多个领域中发挥重要作用?它的哪些特性是其成功的关键?
-
在实际项目中,你认为Python最擅长解决哪些问题?它的局限性又在哪里?
-
同学们,Python是你们通向编程世界的一把金钥匙。掌握它,你将拥有强大的生产力,能够编写出各种各样的应用程序,并为后续更高级的计算机课程学习奠定坚实的基础。
至此,我们已经完成了第三阶段**“全栈应用开发实战”的第一课“编程基础 - Python”的所有内容。接下来,我们将继续深入数据结构**,学习如何用更优化的方式组织和管理数据。请大家稍作休息,我们稍后继续。
好的,同学们,我们继续第三阶段**“全栈应用开发实战”的学习!上一节我们全面掌握了Python编程的基础,能够编写出功能完整的程序。现在,我们将把目光投向“如何更高效地组织和存储数据”——这就是数据结构**的魅力所在。
数据结构是计算机科学的基石,它与算法紧密相连,是解决问题和编写高性能程序的关键。大家可以把数据结构想象成不同的“数据仓库”或“容器”,每种容器都有其独特的存储方式和存取规则,选择合适的容器能让你的程序运行得更快、更节省资源。
课程3.2:数据结构基础(超详细版)
一、数据结构基础概念:理解数据的“排列组合”
1.1 什么是数据结构
**数据结构(Data Structure)**是“组织数据”的方式和规则。它定义了数据元素之间的关系(逻辑结构),以及数据在计算机内存中的存储方式(物理结构),并影响了对数据进行操作(如增、删、改、查)的效率。
-
比喻:如果你有一堆书,数据结构就是你如何把这些书排列起来。
-
按照书名首字母顺序排(线性结构)?
-
按照作者姓氏排(线性结构)?
-
按照类别分层摆放(树形结构)?
-
按照内容关联性用线连接起来(图结构)?
-
每种排列方式都会影响你查找某本书的速度。
-
1.2 数据结构的分类:线性的与非线性的
数据结构可以根据其逻辑结构(数据元素之间的关系)大致分为两类:
-
线性结构(Linear Structure):
-
特点:数据元素之间存在一对一的线性关系,就像排队一样,每个元素只有一个直接前驱和(或)一个直接后继。
-
例子:数组、链表、栈、队列。
-
-
非线性结构(Non-linear Structure):
-
特点:数据元素之间存在一对多(层次关系)或多对多(网状关系)的复杂关系。
-
例子:树(Tree)、图(Graph)。
-
-
特殊结构:
- 在上述基础上,还有一些特殊或复合的数据结构,如堆(Heap)、哈希表(Hash Table)、并查集(Disjoint Set)、**字典树(Trie)**等。
1.3 数据结构与算法的关系:相辅相成,缺一不可
-
数据结构是算法操作的“材料”:没有数据,算法就无法工作。数据结构为算法提供了组织和存储数据的方式。
-
算法是操作数据结构的“方法”:有了数据结构,我们还需要定义如何对这些数据进行操作(如排序、查找)。
-
“程序 = 数据结构 + 算法”:这是著名计算机科学家Niklaus Wirth提出的经典公式。
-
效率关键:解决同一个问题,选择不同的数据结构和算法,其运行效率(时间)和资源消耗(空间)可能天壤之别。例如,在1亿个数字中查找一个特定数字,用有序数组+二分查找会比无序数组+线性查找快N倍。
二、线性结构详解:像链子一样排队的数据
线性结构是最简单也最常用的数据组织方式。
2.1 数组(Array):连续存储的“大盒子”
数组是一种最基本、最常用的数据结构。
2.1.1 原理与特性
-
连续存储:数组将元素存储在内存中一块连续的、预分配的空间中。每个元素的大小通常是固定的。
-
定长/变长:
-
在C/C++/Java等语言中,数组通常是定长的,一旦创建大小就固定。
-
在Python中,
list(列表)本质上是动态变长数组的实现(底层是C语言数组,会根据需要自动扩容)。
-
-
随机访问(Random Access):
-
这是数组最重要的特性。由于内存连续,可以通过元素的下标(索引)直接计算出元素的物理地址,因此访问数组中的任何一个元素的时间复杂度都是O(1)(常数时间)。
-
比喻:就像你有很多编号连续的房间,你知道房间号,就能直接找到这个房间。
-
-
插入、删除效率低:
-
如果要在数组的中间插入或删除一个元素,为了保持内存的连续性,其后的所有元素都需要进行整体移动。这会导致时间复杂度为O(n)(线性时间),
n为元素数量。 -
比喻:在排好队的人中间插入一个人,后面所有的人都得往后挪一位。
-
2.1.2 Python实现与应用
-
Python列表(list):是Python内置的最常用的数据类型,它实现了动态变长数组的功能。
-
多维数组:Python列表可以嵌套,形成多维数组。在科学计算中,NumPy库提供了高效的多维数组对象(
ndarray),支持大量数值运算。
# Python列表作为数组
arr = [10, 20, 30, 40, 50]
# 访问元素(随机访问,O(1))
print(arr[2]) # 输出 30
# 修改元素
arr[0] = 5 # arr变为 [5, 20, 30, 40, 50]
# 在中间插入元素(O(n))
arr.insert(1, 15) # arr变为 [5, 15, 20, 30, 40, 50]
# 删除中间元素(O(n))
del arr[3] # 删除索引3的元素(30),arr变为 [5, 15, 20, 40, 50]
# 在末尾添加元素(Python列表底层优化,通常O(1))
arr.append(60) # arr变为 [5, 15, 20, 40, 50, 60]
print(arr)
2.1.3 常见应用
-
实现栈、队列等其他线性数据结构的底层。
-
批量数据的高效存储与索引,如图像处理中的像素数据、矩阵运算、表格数据等。
-
查找表:当数据量不大且需要快速随机访问时。
2.1.4 时间复杂度总结(数组)
| 操作 | 复杂度 | 说明 |
|-------------|--------|-------------------------------------|
| 访问(按索引)| O(1) | 直接计算地址,一步到位。 |
| 插入/删除(中间)| O(n) | 需要移动后续所有元素。 |
| 插入/删除(末尾)| O(1)* | Python列表底层优化,通常是常数时间。|
| 查找(无序)| O(n) | 需要遍历整个数组。 |
*这里的O(1)是平均情况。当Python列表需要扩容时,可能会发生O(n)的拷贝操作,但这种操作摊还到每次插入上,平均复杂度仍为O(1)。
2.2 链表(Linked List):灵活变化的“珠子项链”
链表是另一种重要的线性数据结构,与数组截然不同。
2.2.1 原理与特性
-
非连续存储:链表中的元素(节点)在内存中可以不连续存放。每个节点除了包含数据(Data)外,还包含一个指向下一个节点地址的指针(Pointer)。
-
通过指针连接:元素之间通过指针(或引用)逻辑上连接起来,形成一个链条。
-
高效的插入、删除:
-
在已知要插入/删除的节点位置(或其前一个节点)的情况下,只需修改少数几个节点的指针,即可完成插入或删除操作。时间复杂度为O(1)(常数时间)。
-
比喻:就像你在一串珠子项链的中间插入或取下一颗珠子,你只需要打开并重新扣上两个连接点,不需要移动整串项链。
-
-
低效的随机访问:
-
如果想访问链表中的第N个元素,你必须从头节点开始,沿着指针依次遍历N-1次才能找到。因此,随机访问的时间复杂度是O(n)。
-
比喻:你想找到第50页的书,却不能直接翻,只能一页页地从第1页开始翻。
-
2.2.2 类型
-
单向链表(Singly Linked List):
- 每个节点只包含一个指向下一个节点的指针。只能从头到尾遍历。
-
双向链表(Doubly Linked List):
-
每个节点包含一个指向下一个节点的指针和一个指向前一个节点的指针。可以向前或向后遍历。
-
优点:在知道某个节点后,可以快速访问其前驱节点,方便删除操作(因为删除一个节点需要知道它的前驱节点)。
-
-
循环链表(Circular Linked List):
-
链表的尾节点指向头节点,形成一个环。
-
优点:可以从任何节点开始遍历整个链表,没有明确的起点和终点。
-
2.2.3 Python实现(示例:单向链表)
在Python中,我们可以通过定义类来模拟链表的节点结构。
class Node:
def __init__(self, val):
self.val = val # 节点存储的数据
self.next = None # 指向下一个节点的指针,初始为None
class LinkedList:
def __init__(self):
self.head = None # 链表的头节点,初始为空
def insert_at_beginning(self, val):
"""在链表头部插入一个节点 (O(1))"""
new_node = Node(val)
new_node.next = self.head
self.head = new_node
def append(self, val):
"""在链表尾部添加一个节点 (O(n), 如果没有维护尾指针)"""
new_node = Node(val)
if self.head is None:
self.head = new_node
return
current = self.head
while current.next:
current = current.next
current.next = new_node
def delete_node(self, key):
"""删除链表中第一个值为key的节点 (O(n))"""
current = self.head
prev = None
while current and current.val != key:
prev = current
current = current.next
if current is None: # 未找到节点
return False
if prev is None: # 如果是头节点
self.head = current.next
else:
prev.next = current.next
return True
def display(self):
"""遍历并打印链表所有元素"""
current = self.head
elements = []
while current:
elements.append(str(current.val))
current = current.next
print(" -> ".join(elements) + " -> None")
# 使用示例
ll = LinkedList()
ll.insert_at_beginning(3) # 3 -> None
ll.insert_at_beginning(2) # 2 -> 3 -> None
ll.insert_at_beginning(1) # 1 -> 2 -> 3 -> None
ll.display() # 输出: 1 -> 2 -> 3 -> None
ll.append(4) # 1 -> 2 -> 3 -> 4 -> None
ll.display() # 输出: 1 -> 2 -> 3 -> 4 -> None
ll.delete_node(2) # 1 -> 3 -> 4 -> None
ll.display() # 输出: 1 -> 3 -> 4 -> None
ll.delete_node(1) # 3 -> 4 -> None
ll.display() # 输出: 3 -> 4 -> None
2.2.4 典型应用
-
实现队列、哈希表的拉链法冲突解决:链表用于存储哈希冲突的元素。
-
操作系统的任务调度、内存管理链表:链表用于维护各种任务队列、空闲内存块列表。
-
LRU缓存(Least Recently Used):
- 通常用双向链表和哈希表的组合来实现。链表维护访问顺序,哈希表用于快速查找。每次访问元素时,将其移到链表头部;当缓存满时,移除链表尾部的元素。
2.2.5 时间复杂度总结(链表)
| 操作 | 复杂度 | 说明 |
|-------------|--------|-------------------------------------|
| 访问(按索引)| O(n) | 必须从头开始遍历。 |
| 插入/删除(已知位置)| O(1) | 只需修改指针。需要先找到位置,这部分可能是O(n)。|
| 查找 | O(n) | 需要遍历整个链表。 |
2.3 栈(Stack):后进先出的“堆叠盘子”
栈是一种特殊的线性数据结构,遵循**后进先出(LIFO, Last In, First Out)**的原则。
2.3.1 原理与特性
-
LIFO:最后进入的元素最先出来。
-
单端操作:所有插入和删除操作都只在栈的**一端(栈顶,Top)**进行。
-
基本操作:
-
push:向栈顶添加元素(入栈)。 -
pop:从栈顶移除元素(出栈)。 -
peek/top:查看栈顶元素,但不移除。 -
is_empty:检查栈是否为空。
-
2.3.2 Python实现
-
用列表(list)实现:Python的列表非常适合模拟栈。
-
append():作为push(入栈)。 -
pop():作为pop(出栈)。
-
-
collections.deque:更高效的双端队列,也可以用作栈,性能更好。
# 使用Python列表实现栈
stack = []
# 入栈 (Push)
stack.append(1)
stack.append(2)
stack.append(3)
print(f"入栈后: {stack}") # [1, 2, 3]
# 查看栈顶 (Peek/Top)
if stack:
print(f"栈顶元素: {stack[-1]}") # 3
# 出栈 (Pop)
popped_element = stack.pop()
print(f"出栈元素: {popped_element}, 栈变为: {stack}") # 3, [1, 2]
popped_element = stack.pop()
print(f"出栈元素: {popped_element}, 栈变为: {stack}") # 2, [1]
# 检查栈是否为空
print(f"栈是否为空: {not stack}") # False
2.3.3 典型应用
-
浏览器前进/后退功能:每次浏览一个新页面就入栈,点击后退则出栈。
-
递归调用栈:程序函数调用时,每次函数调用都会将其上下文(参数、局部变量、返回地址)压入调用栈,函数执行完毕后从栈中弹出。这是递归得以实现的基础。
-
表达式求值:如将中缀表达式转换为后缀表达式(逆波兰表示法)并求值。
-
括号匹配:检查表达式中的括号是否正确匹配(如
([]))。 -
文本编辑器中的撤销(Undo)操作。
2.3.4 时间复杂度总结(栈)
| 操作 | 复杂度 |
|-------------|--------|
| 入栈 (Push) | O(1) |
| 出栈 (Pop) | O(1) |
| 查看栈顶 | O(1) |
2.4 队列(Queue):先进先出的“排队”
队列是另一种特殊的线性数据结构,遵循**先进先出(FIFO, First In, First Out)**的原则。
2.4.1 原理与特性
-
FIFO:最先进入的元素最先出来。
-
两端操作:
-
入队(Enqueue):在队列的**尾部(Rear)**添加元素。
-
出队(Dequeue):在队列的**头部(Front)**移除元素。
-
-
比喻:就像排队买票,先排队的人先买到票。
2.4.2 Python实现
-
用列表(list)实现(不推荐用于大量操作):
-
append():作为入队(在末尾添加)。 -
pop(0):作为出队(从头部移除)。 -
老师提示:
list.pop(0)由于需要移动所有后续元素,在列表较大时性能是O(n),效率较低。
-
-
collections.deque(推荐):双端队列,支持两端高效添加和移除,适合做队列。-
append():入队。 -
popleft():出队。
-
-
queue.Queue(推荐,用于多线程环境):Python标准库queue模块提供了线程安全的队列实现,适合在并发编程中使用。
from collections import deque
# 使用 collections.deque 实现队列
q = deque()
# 入队 (Enqueue)
q.append(1)
q.append(2)
q.append(3)
print(f"入队后: {q}") # deque([1, 2, 3])
# 出队 (Dequeue)
dequeued_element = q.popleft()
print(f"出队元素: {dequeued_element}, 队列变为: {q}") # 1, deque([2, 3])
dequeued_element = q.popleft()
print(f"出队元素: {dequeued_element}, 队列变为: {q}") # 2, deque([3])
# 检查队列是否为空
print(f"队列是否为空: {not q}") # False
2.4.3 变种
-
循环队列:用数组实现,队尾指针达到数组末尾时,可以循环回到数组开头继续入队,避免“假溢出”问题(数组尾部有空间但无法利用)。
-
双端队列(Deque):两端都允许插入和删除,
collections.deque就是其实现。 -
优先队列(Priority Queue):根据元素的优先级决定出队顺序(通常用堆实现,后面会讲)。
2.4.4 应用
-
操作系统的进程/线程调度:任务调度队列。
-
广度优先搜索(BFS)算法:用于遍历图或树。
-
缓存:如网页访问队列、打印任务队列。
-
流量控制、消息队列:在分布式系统中,用于缓冲突发流量,解耦生产者和消费者。
2.4.5 时间复杂度总结(队列,用deque实现)
| 操作 | 复杂度 |
|-------------|--------|
| 入队 (Enqueue) | O(1) |
| 出队 (Dequeue) | O(1) |
同学们,线性数据结构是我们编程中最常用、最基础的“收纳盒”。理解它们各自的特点、优缺点和适用场景,是你们高效解决问题的第一步。
好的,同学们,我们继续数据结构基础的学习。上一节我们全面学习了线性数据结构,包括数组、链表、栈和队列。现在,我们将进入更复杂的非线性数据结构的世界,首先从具有层次关系的树结构开始。
大家可以把线性结构想象成排成一列的队伍,或是一串项链,数据之间只有简单的一对一关系。但现实世界中,很多数据是具有层次关系的,比如公司的组织架构、文件系统目录、家族谱系等。这时,树结构就成为了最合适的模型。
三、树结构详解:层次化的“组织者”
树是一种重要的非线性数据结构,它模拟了具有层次关系的现实世界实体。
3.1 树的基本概念:家族谱系图
-
定义:树是由
n(n>=0)个节点(Node)组成的有限集合。当n=0时称为空树;当n>0时,它满足以下条件:-
有且仅有一个特定的称为**根(Root)**的节点。
-
其余
n-1个节点可以划分为m(m>=0)个互不相交的有限集合T1, T2, ..., Tm,其中每一个集合又都是一棵树,并且称为根的子树(Subtree)。
-
-
基本概念:
-
节点(Node):树的基本组成单位,存储数据。
-
根节点(Root Node):树中最顶层的节点,没有父节点。一棵树只有一个根节点。
-
父节点(Parent Node):某节点的上层节点。
-
子节点(Child Node):某节点的下层节点。
-
兄弟节点(Sibling Nodes):拥有相同父节点的节点。
-
叶子节点(Leaf Node):没有子节点的节点。
-
非叶子节点/分支节点:有子节点的节点。
-
边(Edge):连接两个节点的线。
-
路径(Path):从一个节点到另一个节点所经过的边的序列。
-
深度(Depth):从根节点到某节点的路径长度(根节点深度为0)。
-
高度(Height):从某节点到最远叶子节点的路径长度(叶子节点高度为0)。通常指树的高度,即根节点的高度。
-
层级(Level):节点所处的层次(根节点在第1层,其子节点在第2层,以此类推)。
-
子树(Subtree):树中任一节点及其所有后代节点组成的结构。
-
-
比喻:
-
家族谱系图:爷爷是根节点,爸爸是子节点,你是孙子节点。
-
公司组织架构图:CEO是根节点,各部门经理是子节点。
-
文件系统目录结构:根目录
/是根节点,文件夹是分支节点,文件是叶子节点。
-
3.2 二叉树:最常用的树结构
**二叉树(Binary Tree)**是树结构中一种特殊且极其常用的类型。
- 定义:每个节点最多只有两个子节点,分别称为左子节点(Left Child)和右子节点(Right Child)。左右子节点的位置是固定的,不能互换。
3.2.1 遍历方式:访问树的“路线图”
遍历二叉树是指按照某种顺序访问树中的所有节点。主要有四种经典遍历方式:
-
深度优先遍历(Depth-First Traversal, DFS):
-
特点:沿着树的深度方向,尽可能深地访问节点。
-
三种顺序:
-
前序遍历(Pre-order Traversal):根 -> 左 -> 右
-
访问根节点
-
前序遍历左子树
-
前序遍历右子树
-
比喻:领导视察,先去看自己,再去看左边的儿子,再去看右边的儿子。
-
应用:复制一棵树,构建表达式树。
-
-
中序遍历(In-order Traversal):左 -> 根 -> 右
-
中序遍历左子树
-
访问根节点
-
中序遍历右子树
-
比喻:一家人吃饭,先让左边的儿子吃,再自己吃,再让右边的儿子吃。
-
应用:对于二叉搜索树,中序遍历结果是有序序列。
-
-
后序遍历(Post-order Traversal):左 -> 右 -> 根
-
后序遍历左子树
-
后序遍历右子树
-
访问根节点
-
比喻:一家人吃饭,先让左边的儿子吃,再让右边的儿子吃,最后自己吃(长辈关爱晚辈)。
-
应用:删除一棵树,计算表达式树的值。
-
-
-
-
层序遍历(Level-order Traversal)/广度优先搜索(BFS):
-
特点:按照树的层级从上到下、从左到右依次访问节点。
-
实现:通常使用队列来辅助实现。
-
比喻:一家人吃饭,先让第一代(根)吃,再让第二代(根的子节点)吃,再让第三代(孙子节点)吃,依次类推。
-
应用:查找最短路径(如迷宫问题)、构建层级菜单。
-
3.2.3 Python实现(示例:二叉树节点与遍历)
class TreeNode:
def __init__(self, val):
self.val = val # 节点值
self.left = None # 左子节点指针
self.right = None # 右子节点指针
# 构建一个示例二叉树:
# 1
# / \
# 2 3
# / \
# 4 5
root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.left.left = TreeNode(4)
root.left.right = TreeNode(5)
print("--- 前序遍历 (根左右) ---")
def preorder_traversal(node):
if node:
print(node.val, end=" ")
preorder_traversal(node.left)
preorder_traversal(node.right)
preorder_traversal(root) # 输出: 1 2 4 5 3
print("\n--- 中序遍历 (左根右) ---")
def inorder_traversal(node):
if node:
inorder_traversal(node.left)
print(node.val, end=" ")
inorder_traversal(node.right)
inorder_traversal(root) # 输出: 4 2 5 1 3
print("\n--- 后序遍历 (左右根) ---")
def postorder_traversal(node):
if node:
postorder_traversal(node.left)
postorder_traversal(node.right)
print(node.val, end=" ")
postorder_traversal(root) # 输出: 4 5 2 3 1
print("\n--- 层序遍历 (广度优先) ---")
from collections import deque
def levelorder_traversal(root_node):
if not root_node:
return
q = deque([root_node])
while q:
node = q.popleft()
print(node.val, end=" ")
if node.left:
q.append(node.left)
if node.right:
q.append(node.right)
levelorder_traversal(root) # 输出: 1 2 3 4 5
print("\n")
3.2.2 典型应用
-
表达式树:用于表示数学表达式,方便求值或转换。
-
二叉查找树:一种特殊的二叉树,用于高效查找、插入、删除数据。
-
堆:一种特殊的完全二叉树,用于实现优先队列。
-
哈夫曼编码树:用于数据压缩。
-
文件系统目录结构(广义的树形结构)。
3.3 二叉搜索树(Binary Search Tree, BST):高效查找的“有序树”
二叉搜索树(也叫二叉查找树)是一种特殊的二叉树。
-
定义:对于树中的任意一个节点:
-
其左子树中所有节点的值都小于该节点的值。
-
其右子树中所有节点的值都大于该节点的值。
-
左右子树也必须是二叉搜索树。
-
-
优点:结合了链表的插入删除效率和有序数组的查找效率(对数级别)。
-
核心特性:中序遍历一棵二叉搜索树,得到的结果是有序的。
3.3.1 操作
-
插入(Insert):
-
从根节点开始,如果要插入的值小于当前节点,则向左子树查找;如果大于,则向右子树查找。
-
直到找到一个空位置(
None),将新节点插入。 -
时间复杂度:平均O(log n),最坏O(n)(当树退化为链表时)。
-
-
查找(Search):
-
从根节点开始,如果要查找的值等于当前节点,则找到;如果小于,则向左子树查找;如果大于,则向右子树查找。
-
时间复杂度:平均O(log n),最坏O(n)。
-
-
删除(Delete):
-
最复杂的操作,需要分三种情况:
-
删除叶子节点:直接删除。
-
删除只有一个子节点的节点:将该节点的子节点直接连接到其父节点。
-
删除有两个子节点的节点:用其右子树的最小节点(或左子树的最大节点)来替换被删除节点,然后删除那个替换用的节点。
-
-
时间复杂度:平均O(log n),最坏O(n)。
-
3.3.2 不足
-
极端情况下的性能退化:如果插入的元素本身就是有序的(如1, 2, 3, 4, 5),二叉搜索树会退化成一个“斜”的链表。此时,查找、插入、删除操作的效率将从O(log n)退化为O(n),失去了优势。
- 比喻:你把书架上的书都斜着放,找起来就和一摞书一样麻烦了。
3.4 平衡树(AVL树、红黑树):追求“完美”的有序
为了解决二叉搜索树在极端情况下性能退化的问题,出现了平衡二叉搜索树(Self-Balancing Binary Search Tree)。它们在插入和删除操作后,会自动调整树的结构(通过旋转等操作),以保持树的平衡,从而保证所有操作的最坏时间复杂度依然是O(log n)。
3.4.1 AVL树
-
定义:最早的自平衡二叉搜索树。它要求任意节点的左右子树高度差的绝对值不超过1。
-
特点:在插入或删除节点后,如果平衡性被破坏,需要通过左旋、右旋等操作来重新调整树的结构,使其恢复平衡。
-
优点:严格的平衡,查找效率高。
-
缺点:维护平衡的开销较大,插入删除操作可能涉及多次旋转。
3.4.2 红黑树(Red-Black Tree)
-
定义:一种更常用、更复杂的自平衡二叉搜索树。它通过为每个节点引入“颜色”(红色或黑色)并遵循一系列规则来保持近似平衡。
-
特点:
-
规则相对复杂,但插入和删除操作平均只需要常数次旋转,效率高。
-
不是严格平衡(高度差可能大于1),但最长路径不大于最短路径的两倍,保证了O(log n)的性能。
-
-
应用:被广泛应用于实际系统中:
-
C++ STL(Standard Template Library)中的
std::map和std::set -
Java中的
TreeMap和TreeSet -
Linux内核中的进程调度、内存管理等。
-
3.5 堆(Heap):优先级的“排队机”
堆是一种特殊的完全二叉树。
-
定义:
-
完全二叉树:除最后一层外,其他层都是满的,最后一层从左到右填充。
-
堆特性:
-
大根堆(Max-Heap):每个父节点的值都大于或等于其所有子节点的值。堆顶元素是最大值。
-
小根堆(Min-Heap):每个父节点的值都小于或等于其所有子节点的值。堆顶元素是最小值。
-
-
-
实现:通常使用数组来实现,因为完全二叉树的特性使得节点和数组索引之间存在简单的映射关系。
-
基本操作:
-
插入(Push/heappush):将新元素添加到数组末尾,然后通过“上浮”(Heapify Up)操作将其调整到正确位置,保持堆特性。
-
删除堆顶(Pop/heappop):移除堆顶元素,将末尾元素移到堆顶,然后通过“下沉”(Heapify Down)操作将其调整到正确位置。
-
-
Python实现:Python的
heapq模块提供了小根堆的实现。
import heapq
# 创建一个空列表,heapq模块将其视为堆
min_heap = []
# 插入元素 (heappush)
heapq.heappush(min_heap, 3) # 堆变为 [3]
heapq.heappush(min_heap, 1) # 堆变为 [1, 3] (内部是数组,不是真正的树结构,但逻辑上符合堆特性)
heapq.heappush(min_heap, 5) # 堆变为 [1, 3, 5] (可能为 [1,5,3] 具体取决于实现)
heapq.heappush(min_heap, 2) # 堆变为 [1, 2, 5, 3]
print(f"当前堆: {min_heap}") # 实际数组表示可能为 [1, 2, 5, 3]
# 弹出堆顶元素 (heappop),总是最小的元素
smallest_element = heapq.heappop(min_heap)
print(f"弹出的最小元素: {smallest_element}, 堆变为: {min_heap}") # 1, 堆变为 [2, 3, 5]
smallest_element = heapq.heappop(min_heap)
print(f"弹出的最小元素: {smallest_element}, 堆变为: {min_heap}") # 2, 堆变为 [3, 5]
-
典型应用:
-
优先队列(Priority Queue):堆是实现优先队列最有效的数据结构。在优先队列中,元素不是按照先进先出(FIFO)或后进先出(LIFO)的顺序出队,而是按照它们的优先级高低出队。
-
堆排序(Heap Sort):一种时间复杂度为O(n log n)的排序算法。
-
TOP K问题:从N个元素中找出最大/最小的K个元素,通常用大小为K的堆来实现。
-
同学们,树结构及其各种变种是处理具有层次关系数据和进行高效查找的关键。理解二叉树、二叉搜索树、平衡树和堆的原理与应用,将极大地提升你们在算法设计和问题解决中的能力。
好的,同学们,我们继续数据结构基础的学习。前几节课,我们全面探讨了线性数据结构(数组、链表、栈、队列)和非线性数据结构(树、堆、哈希表、图)。现在,我们将学习一些特殊但非常有用的数据结构,并深入掌握衡量算法效率的关键工具——时间复杂度和空间复杂度分析。
这些特殊数据结构往往是前面基础结构的组合或变种,它们针对特定问题提供了更优化的解决方案。而复杂度分析,则是我们评判一个算法好坏的唯一客观标准,是算法工程师的“火眼金睛”。
六、特殊结构:为特定问题而生
6.1 并查集(Disjoint Set / Union-Find):高效处理集合合并与查询
-
概念:并查集是一种用于处理不相交集合(Disjoint Sets)的数据结构,它支持两种主要操作:
-
查找(Find):确定元素所属的集合(通常返回该集合的代表元素)。
-
合并(Union):将两个不相交的集合合并为一个集合。
-
-
用途:常用于解决连通性问题(如判断图中两个节点是否连通)、最小生成树算法(Kruskal算法)、网络连接判断等。
-
核心思想:每个集合都用一棵树来表示,树的根节点是该集合的代表元素。每个节点存储其父节点的信息。
-
优化:
-
路径压缩(Path Compression):在执行
Find操作时,将路径上的所有节点直接连接到根节点,从而扁平化树结构,加速后续查找。 -
按秩合并(Union by Rank/Size):在执行
Union操作时,总是将较小的树连接到较大的树下面,以保持树的平衡,减少树的高度。
-
-
时间复杂度:经过路径压缩和按秩合并优化的并查集,其操作的平均时间复杂度接近O(α(n)),其中α(n)是阿克曼函数的反函数,增长非常缓慢,可以近似认为是常数时间O(1)。
-
Python实现(简化版):
class UnionFind:
def __init__(self, n):
# 初始化:每个元素都是一个独立的集合,其父节点指向自身
self.parent = list(range(n))
# self.rank = [0] * n # 可选:用于按秩合并优化,这里简化未实现
def find(self, x):
"""查找元素x的根节点(代表元素),并进行路径压缩"""
if self.parent[x] == x: # 如果x的父节点是自身,说明x就是根节点
return x
# 路径压缩:将当前节点直接连接到根节点
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x, y):
"""合并x和y所在的两个集合"""
root_x = self.find(x)
root_y = self.find(y)
if root_x != root_y: # 如果它们不在同一个集合中,则进行合并
# 简单的合并:将x的根节点连接到y的根节点
self.parent[root_x] = root_y
# 这里可以添加按秩合并的逻辑来优化树的结构
return True # 合并成功
return False # 已经在同一个集合中,无需合并
# 使用示例
uf = UnionFind(10) # 0, 1, ..., 9 共10个元素,初始各自独立
# 合并一些集合
uf.union(0, 1) # 合并 {0,1}
uf.union(2, 3) # 合并 {2,3}
uf.union(4, 5) # 合并 {4,5}
uf.union(0, 2) # 合并 {0,1} 和 {2,3} -> {0,1,2,3}
uf.union(6, 7) # 合并 {6,7}
print(f"元素0的根节点: {uf.find(0)}") # 应该与1,2,3的根节点相同
print(f"元素1的根节点: {uf.find(1)}")
print(f"元素3的根节点: {uf.find(3)}")
print(f"元素4的根节点: {uf.find(4)}")
print(f"元素7的根节点: {uf.find(7)}")
print(f"0和3是否连通: {uf.find(0) == uf.find(3)}") # True
print(f"0和4是否连通: {uf.find(0) == uf.find(4)}") # False
6.2 字典树(Trie / Prefix Tree):字符串查找的“前缀专家”
-
概念:字典树,又称前缀树,是一种用于存储大量字符串的数据结构。它以节点来表示字符串的公共前缀,从根节点到任意一个节点的路径都对应着一个字符串。
-
用途:
-
高效存储和检索字符串集合:尤其适用于需要进行前缀查找、自动补全、拼写检查、敏感词过滤等场景。
-
时间复杂度:查找和插入一个字符串的时间复杂度与字符串的长度
L成正比(O(L)),与字典中字符串的数量无关。这比哈希表和平衡树在字符串查找方面更快。
-
-
比喻:就像一棵树,每个分支代表一个字母,沿着分支走就能拼出单词。例如,你从根节点开始走
'A',然后是'P',你就找到了前缀AP,再走'P',就找到了APP。 -
Python实现(简化版):
class TrieNode:
def __init__(self):
self.children = {} # 存储子节点,键是字符,值是TrieNode
self.is_end_of_word = False # 标记是否是一个单词的结尾
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word):
"""插入一个单词"""
node = self.root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_end_of_word = True # 标记单词结尾
def search(self, word):
"""查找一个单词是否存在"""
node = self.root
for char in word:
if char not in node.children:
return False
node = node.children[char]
return node.is_end_of_word # 确保是单词结尾,而不是某个单词的前缀
def starts_with(self, prefix):
"""检查是否有以给定前缀开头的单词"""
node = self.root
for char in prefix:
if char not in node.children:
return False
node = node.children[char]
return True
# 使用示例
trie = Trie()
trie.insert("apple")
trie.insert("apply")
trie.insert("apricot")
trie.insert("banana")
print(f"搜索 'apple': {trie.search('apple')}") # True
print(f"搜索 'app': {trie.search('app')}") # False (因为app不是一个完整的单词)
print(f"搜索 'banana': {trie.search('banana')}") # True
print(f"搜索 'orange': {trie.search('orange')}") # False
print(f"以 'ap' 开头的单词是否存在: {trie.starts_with('ap')}") # True
print(f"以 'ban' 开头的单词是否存在: {trie.starts_with('ban')}") # True
print(f"以 'ora' 开头的单词是否存在: {trie.starts_with('ora')}") # False
七、时间复杂度与空间复杂度分析:算法的“体检报告”
在计算机科学中,我们不仅仅要解决问题,更要高效地解决问题。时间复杂度和空间复杂度是衡量算法效率和资源消耗的两个最基本、最重要的指标。
7.1 大O符号(Big O Notation):描述增长趋势
-
定义:大O符号是一种渐进表示法,用于描述算法的运行时间或所需空间与输入规模(n)之间的增长关系。它表示当输入规模
n趋于无穷大时,算法的增长趋势。我们通常只关注算法的最坏情况时间复杂度。 -
作用:它关注的是算法的效率级别,而不是精确的运行时间。它让我们能够比较不同算法的优劣,而与具体的硬件、编程语言无关。
-
忽略常数和低阶项:在计算大O时,我们会忽略常数、系数和低阶项,因为当
n足够大时,它们的影响可以忽略不计。-
例如:
O(2n + 5)->O(n) -
例如:
O(3n^2 + 100n + 1000)->O(n^2)
-
7.1.1 常见复杂度级别
理解这些常见的复杂度级别,是评估算法效率的关键。
| 大O符号 | 描述 | 增长趋势 | 常见操作举例 | 形象比喻 |
|------------|--------------|-----------------------|------------------------------|---------------------------------|
| O(1) | 常数时间 | 极快,不随n变化 | 访问数组元素、哈希表查找 | 一步到位,无论有多少本书,我知道第5页在哪。|
| O(log n)| 对数时间 | 增长非常缓慢,高效 | 二分查找 | 每次排除一半,像查字典一样。 |
| O(n) | 线性时间 | 随n线性增长 | 遍历列表、无序数组查找 | 挨个检查,就像找房间号。 |
| O(n log n)| 线性对数时间| 相对高效,复杂排序 | 快速排序、归并排序、堆排序 | “效率较高”的排序。 |
| O(n²) | 平方时间 | 增长较快,效率低 | 冒泡排序、选择排序、两层嵌套循环| 两次遍历,像从一群人中找两两组合。|
| O(2ⁿ) | 指数时间 | 增长极快,效率极低 | 递归算法(如斐波那契数列未优化)| 每增加一个数据,计算量翻倍。 |
| O(n!) | 阶乘时间 | 增长恐怖,基本不可用 | 旅行商问题(暴力) | 计算所有可能排列组合。 |
7.2 时间复杂度(Time Complexity):衡量算法的“速度”
-
定义:指算法执行所消耗的时间,通常用基本操作的执行次数来表示。我们关注的是算法随着输入规模
n的增加,其运行时间变化的趋势。 -
分析思路:
-
识别基本操作:找出算法中最耗时的操作(如比较、赋值、算术运算、数组访问等)。
-
统计操作次数:根据输入规模
n,计算基本操作的执行总次数。 -
使用大O符号表示:忽略常数和低阶项,得出最终的大O表示。
-
-
举例:
-
遍历列表:
def find_element(arr, target): for x in arr: # 假设arr有n个元素 if x == target: return True return False-
基本操作:
x == target(比较),最多执行n次。 -
时间复杂度:O(n)。
-
-
嵌套循环:
def matrix_multiply(matrix_a, matrix_b, n): # 假设是n x n矩阵相乘 result_matrix = [[0 for _ in range(n)] for _ in range(n)] for i in range(n): for j in range(n): for k in range(n): result_matrix[i][j] += matrix_a[i][k] * matrix_b[k][j] return result_matrix-
基本操作:
+=和*,执行了n * n * n = n^3次。 -
时间复杂度:O(n^3)。
-
-
递归函数:
-
斐波那契数列(未优化):
fib(n) = fib(n-1) + fib(n-2)。其时间复杂度为O(2^n)。 -
可以用递归树、递推关系式或主定理等方法分析。
-
-
7.3 空间复杂度(Space Complexity):衡量算法的“内存消耗”
-
定义:指算法执行所消耗的存储空间,也通常用大O符号表示,描述算法在运行时额外占用的空间(不包括输入本身占用的空间)与输入规模
n的增长关系。 -
分析思路:关注算法执行过程中所使用的辅助空间,如额外的数组、栈、队列、递归调用栈的深度等。
-
举例:
-
冒泡排序:只需要常数个额外变量来交换元素。
- 空间复杂度:O(1)。
-
归并排序:需要一个与输入数组大小相同的临时数组来合并。
- 空间复杂度:O(n)。
-
递归函数:递归深度可能导致栈空间的使用。
- 例如,深度为
n的递归调用,空间复杂度为O(n)。
- 例如,深度为
-
最好/最坏/平均复杂度:
-
最好情况(Best Case):算法在最理想输入下的表现。
-
最坏情况(Worst Case):算法在最不利输入下的表现。这是我们通常关注的,因为它代表了算法性能的上限。
-
平均情况(Average Case):所有可能输入的加权平均。这通常最能反映算法的真实性能,但分析起来也最复杂。
同学们,复杂度分析是算法学习的核心灵魂。掌握它,你就能像专业的工程师一样,在编写代码之前就预判其性能瓶颈,并选择最合适的算法和数据结构。
好的,同学们,我们继续数据结构基础的学习。我们已经全面学习了各种线性、非线性以及特殊数据结构,并掌握了时间复杂度和空间复杂度分析这个重要的工具。现在,是时候将这些知识付诸实践了——我们将用Python亲自实现一些基础数据结构,并体会它们的工作原理。
八、实践项目:用Python实现基础数据结构
亲手实现这些数据结构,能够帮助你更深刻地理解它们内部的运作机制、优缺点以及在何种场景下适用。
8.1 单向链表(复习与实践)
我们已经在前面讲链表时给出了实现代码。这里再次强调其核心思想。
核心思想:通过Node类定义节点(数据+指向下一个节点的指针),LinkedList类维护链表的头节点。
class Node:
def __init__(self, val):
self.val = val
self.next = None
class LinkedList:
def __init__(self):
self.head = None
def insert_at_beginning(self, val):
"""在链表头部插入一个节点 (O(1))"""
new_node = Node(val)
new_node.next = self.head
self.head = new_node
def append(self, val):
"""在链表尾部添加一个节点 (O(n), 如果没有维护尾指针)"""
new_node = Node(val)
if self.head is None:
self.head = new_node
return
current = self.head
while current.next:
current = current.next
current.next = new_node
def delete_node(self, key):
"""删除链表中第一个值为key的节点 (O(n))"""
current = self.head
prev = None
while current and current.val != key:
prev = current
current = current.next
if current is None: # 未找到节点
return False
if prev is None: # 如果是头节点
self.head = current.next
else:
prev.next = current.next
return True
def display(self):
"""遍历并打印链表所有元素"""
current = self.head
elements = []
while current:
elements.append(str(current.val))
current = current.next
print(" -> ".join(elements) + " -> None")
print("--- 单向链表实践 ---")
ll = LinkedList()
ll.insert_at_beginning(3)
ll.insert_at_beginning(2)
ll.insert_at_beginning(1)
ll.display() # 输出: 1 -> 2 -> 3 -> None
ll.append(4)
ll.display() # 输出: 1 -> 2 -> 3 -> 4 -> None
ll.delete_node(2)
ll.display() # 输出: 1 -> 3 -> 4 -> None
print("\n")
8.2 栈和队列(复习与实践)
使用Python内置的列表(list)或collections.deque即可简单高效地实现栈和队列。
# 使用Python列表实现栈
class Stack:
def __init__(self):
self.data = []
def is_empty(self):
return len(self.data) == 0
def push(self, x):
self.data.append(x)
print(f"Push {x}: {self.data}")
def pop(self):
if self.is_empty():
print("Stack is empty!")
return None
popped = self.data.pop()
print(f"Pop {popped}: {self.data}")
return popped
def peek(self):
if self.is_empty():
return None
return self.data[-1]
print("--- 栈实践 ---")
s = Stack()
s.push(10)
s.push(20)
print(f"栈顶元素: {s.peek()}") # 20
s.pop()
s.pop()
s.pop() # Stack is empty!
print("\n")
from collections import deque
# 使用 collections.deque 实现队列
class Queue:
def __init__(self):
self.data = deque()
def is_empty(self):
return len(self.data) == 0
def enqueue(self, x):
self.data.append(x)
print(f"Enqueue {x}: {self.data}")
def dequeue(self):
if self.is_empty():
print("Queue is empty!")
return None
dequeued = self.data.popleft() # 从左边出队
print(f"Dequeue {dequeued}: {self.data}")
return dequeued
def front(self):
if self.is_empty():
return None
return self.data[0]
print("--- 队列实践 ---")
q = Queue()
q.enqueue('A')
q.enqueue('B')
print(f"队首元素: {q.front()}") # A
q.dequeue()
q.dequeue()
q.dequeue() # Queue is empty!
print("\n")
8.3 二叉树遍历(复习与实践)
我们已经给出了二叉树节点定义和三种深度优先遍历以及层序遍历的实现。这里再次强调其递归和队列的使用。
class TreeNode:
def __init__(self, val):
self.val = val
self.left = None
self.right = None
# 构建树:
# 1
# / \
# 2 3
# / \
# 4 5
root_tree = TreeNode(1)
root_tree.left = TreeNode(2)
root_tree.right = TreeNode(3)
root_tree.left.left = TreeNode(4)
root_tree.left.right = TreeNode(5)
print("--- 二叉树遍历实践 ---")
print("前序遍历 (根左右):", end=" ")
def preorder_traversal_example(node):
if node:
print(node.val, end=" ")
preorder_traversal_example(node.left)
preorder_traversal_example(node.right)
preorder_traversal_example(root_tree)
print("\n")
print("中序遍历 (左根右):", end=" ")
def inorder_traversal_example(node):
if node:
inorder_traversal_example(node.left)
print(node.val, end=" ")
inorder_traversal_example(node.right)
inorder_traversal_example(root_tree)
print("\n")
print("层序遍历 (广度优先):", end=" ")
from collections import deque # 再次导入deque用于队列
def levelorder_traversal_example(root_node):
if not root_node:
return
q_level = deque([root_node])
while q_level:
node = q_level.popleft()
print(node.val, end=" ")
if node.left:
q_level.append(node.left)
if node.right:
q_level.append(node.right)
levelorder_traversal_example(root_tree)
print("\n\n")
8.4 哈希表(简化实现)
这里我们实现一个最简单的基于拉链法的哈希表,帮助理解其核心冲突解决机制。Python的dict和set是更优化的实现。
class SimpleHashTable:
def __init__(self, size=10):
self.size = size
# 创建一个列表,每个元素又是一个列表,用于拉链法存储冲突元素
self.buckets = [[] for _ in range(self.size)]
def _hash(self, key):
"""简单的哈希函数:使用Python内置hash()并取模"""
return hash(key) % self.size
def set(self, key, value):
"""插入或更新键值对"""
hash_index = self._hash(key)
# 遍历桶中的链表,如果键已存在则更新值
for i, (k, v) in enumerate(self.buckets[hash_index]):
if k == key:
self.buckets[hash_index][i] = (key, value)
print(f"更新 '{key}': {value} 到桶 {hash_index}")
return
# 键不存在,则添加到链表末尾
self.buckets[hash_index].append((key, value))
print(f"添加 '{key}': {value} 到桶 {hash_index}")
def get(self, key):
"""根据键获取值"""
hash_index = self._hash(key)
for k, v in self.buckets[hash_index]:
if k == key:
return v
return None # 键不存在
def delete(self, key):
"""删除键值对"""
hash_index = self._hash(key)
for i, (k, v) in enumerate(self.buckets[hash_index]):
if k == key:
del self.buckets[hash_index][i]
print(f"删除 '{key}' 从桶 {hash_index}")
return True
return False # 键不存在
print("--- 哈希表实践 ---")
ht = SimpleHashTable(size=5) # 桶数量为5
ht.set("apple", 10)
ht.set("banana", 20)
ht.set("cherry", 30) # 假设和apple发生冲突
ht.set("date", 40) # 假设和banana发生冲突
ht.set("elderberry", 50)
print(f"获取 'banana': {ht.get('banana')}") # 20
print(f"获取 'cherry': {ht.get('cherry')}") # 30
print(f"获取 'grape': {ht.get('grape')}") # None
ht.delete("cherry")
print(f"获取 'cherry' 再次: {ht.get('cherry')}") # None
print("\n")
九、与后续课程的逻辑衔接:数据结构是算法与系统的基石
数据结构的重要性不言而喻,它几乎是所有高级计算机科学概念的基石。
-
算法设计与分析:
-
数据结构决定算法效率:所有经典的算法(如排序、查找、搜索、图算法、动态规划)都离不开数据结构的支撑。选择正确的数据结构是设计高效算法的第一步。例如,如果你需要高效查找,会考虑哈希表或二叉搜索树;如果你需要管理任务优先级,会考虑堆。
-
算法实现:你学习的各种算法,最终都需要在特定的数据结构上进行操作。
-
-
后端开发:
-
数据库索引:关系型数据库(如MySQL)的B+树索引,MongoDB的B树索引,都是树形数据结构的应用。
-
缓存:Redis的多种数据结构(字符串、哈希、列表、集合、有序集合)以及LRU缓存机制(双向链表+哈希表)都是数据结构在高性能缓存中的应用。
-
消息队列:消息队列的底层通常是队列或链表结构。
-
API设计:如何组织API返回的数据,也经常用到列表和字典的结构。
-
-
全栈开发:
-
前端状态管理:如Vuex、Pinia、Redux中的状态,其内部结构也常是树形或扁平化的字典。
-
数据处理:前端和后端的数据传输、处理、展示,都涉及到数据的组织和解析,离不开对数据结构的理解。
-
性能优化:无论是前端渲染大数据量列表(虚拟列表),还是后端处理高并发请求,合理选择和实现数据结构是性能优化的关键。
-
-
系统设计:
-
分布式哈希:如一致性哈希在分布式系统中的应用。
-
负载均衡:如何管理服务器列表,如何调度请求。
-
拓扑结构:如网络拓扑、服务依赖关系等都可以用图结构建模。
-
操作系统内部:文件系统、进程调度、内存管理等都大量使用了各种数据结构(如页表、PCB链表、文件索引树)。
-
-
面试与工程开发:
-
数据结构与算法是所有IT公司技术面试的必考内容。
-
在实际工程开发中,理解数据结构能让你在设计系统、编写代码时做出更明智的决策,避免性能陷阱,提高代码质量。
-
十、学习建议与资料拓展:持续实践,不断精进
-
动手实现每一种数据结构:不要只停留在理论层面,通过编写代码亲自实现每一种数据结构的插入、删除、查找、遍历等核心操作,这将加深你的理解。
-
刷题练习:
-
LeetCode (力扣):全球最流行的算法和数据结构练习平台,提供大量题目和多种语言的解法。
-
牛客网、LintCode:国内常用的刷题平台。
-
通过刷题,你会发现数据结构和算法在实际问题中的应用,并锻炼解决问题的能力。
-
-
参考教材:
-
《算法(第四版)》(Algorithms, 4th Edition, Sedgewick & Wayne):经典教材,基于Java,但思想通用。
-
《数据结构与算法分析》(Data Structures and Algorithm Analysis, Mark Allen Weiss):也有Java和C++版,讲解深入。
-
《Python数据结构与算法》(Python Algorithms, Magnus Lie Hetland):专门针对Python的实现。
-
-
推荐视频课程:
-
MIT 6.006 (Introduction to Algorithms):经典免费公开课。
-
国内一些在线教育平台(如极客时间、慕课网、B站)上也有很多高质量的数据结构与算法课程(例如王争老师的《数据结构与算法之美》)。
-
-
持续学习和回顾:数据结构与算法是核心基础,需要反复学习和练习才能真正掌握。
十一、课后练习与思考:挑战自我,深化理解
-
用链表实现栈和队列:
- 用我们自己实现的
Node和LinkedList类,或者直接基于Python的类,分别实现一个栈(Stack)和一个队列(Queue),并测试其基本操作。
- 用我们自己实现的
-
判断链表是否有环:
-
编写一个函数,接收一个链表的头节点作为参数,判断该链表中是否存在环(即链表末尾指向了链表中间的某个节点)。
-
提示:思考“快慢指针”法。
-
-
用字典实现一个简单的LRU缓存:
-
LRU(Least Recently Used,最近最少使用)是一种常见的缓存淘汰策略。当缓存满时,淘汰最近最少使用的元素。
-
提示:LRU缓存通常使用**哈希表(用于快速查找)和双向链表(用于维护访问顺序)**组合实现。你可以简化,用一个字典和一个列表模拟。
-
-
设计一个类,实现二叉查找树的插入、查找和中序遍历:
- 使用
TreeNode类来表示节点,实现一个BinarySearchTree类,包含insert、search、inorder_traversal方法。
- 使用
-
思考题:
-
为什么哈希表在平均情况下查找效率能达到O(1)?在什么情况下它的查找效率会退化?哈希冲突的解决办法有哪些?
-
在实际软件开发中,你认为哪种数据结构被使用得最多?为什么?
-
在你目前的项目或日常生活中,有哪些场景可以用学过的数据结构进行优化或建模?
-
同学们,数据结构是你们提升编程能力、走向高级开发者的必经之路。掌握它,你就能更深入地理解计算机程序如何高效地处理信息。
至此,我们已经完成了第三阶段**“全栈应用开发实战”**的第二课“数据结构基础”的所有内容。接下来,我们将正式进入与数据结构紧密相连的“姐妹课程”——算法设计与分析。请大家稍作休息,我们稍后继续。
好的,同学们,我们继续第三阶段**“全栈应用开发实战”**的学习!上一节我们深入探讨了数据结构,学会了如何有效地组织和管理数据。现在,我们将进入与数据结构紧密相连的“姐妹课程”——算法设计与分析。
如果说数据结构是“数据如何存放”,那么算法就是“如何处理这些数据”。一个巧妙的算法能让你的程序以惊人的速度和效率完成任务。在当今这个数据爆炸的时代,高效的算法是解决复杂问题、提升系统性能的核心利器。
三、算法设计与分析:解决问题的“艺术”与“智慧”
7.1 算法基础和分析方法:理解算法的本质与评判标准
7.1.1 什么是算法
算法(Algorithm)是解决特定问题、完成特定任务的一系列明确、有限的、可执行的步骤或指令序列。它是一个过程,接收一些输入,并产生一些输出。
-
比喻:算法就像一份详细的菜谱。
-
输入:食材、厨具。
-
步骤:洗菜、切菜、炒菜、调味等一系列详细的动作。
-
输出:一道美味的菜肴。
-
-
不同的菜谱(算法)可以用同样的食材(数据结构)做出不同的菜,或者用不同的方式做出同样的菜,但效率和口味可能大相径庭。
7.1.2 算法的特性:一个好算法的标准
一个有效的算法必须具备以下五个基本特性:
-
有穷性(Finiteness):算法必须在执行有限步骤后终止,不能无限循环。
- 例子:循环必须有明确的终止条件。
-
确定性(Definiteness):算法的每一步都必须有明确的定义,不允许有歧义,对于相同的输入必须产生相同的输出。
- 例子:不能说“随便做一些调味”,必须明确“加盐3克,白糖5克”。
-
输入(Input):一个算法有零个或多个输入。这些输入在执行算法之前被给定。
-
输出(Output):一个算法有一个或多个输出。它必须产生一些结果。
-
有效性(Effectiveness):算法的每一步都必须是基本可行的,能够通过有限次基本操作来完成。
- 例子:不能说“瞬间移动到月球”,因为当前技术无法实现。
7.1.3 算法分析目标:如何评价一个算法的好坏?
当我们设计出解决问题的算法后,就需要对其进行评估。我们关注的不仅仅是算法能否解决问题(正确性),更重要的是它解决问题的效率。
-
正确性:算法能否在所有合法输入下都产生正确的输出,并满足问题要求。这是算法的首要目标。
-
时间复杂度(Time Complexity):
- 含义:衡量算法运行所需时间的量级。它反映了算法执行效率,即算法处理输入规模(n)的增加时,运行时间如何增长。
-
空间复杂度(Space Complexity):
- 含义:衡量算法运行所需存储空间的量级。它反映了算法执行过程中,除了输入本身外,额外占用的内存空间如何增长。
-
可读性与可维护性:算法代码是否清晰易懂,方便他人理解和修改。
-
健壮性(Robustness):算法能否在遇到异常输入或边缘情况时,仍能正确处理或给出合理的错误提示。
-
可扩展性:算法是否容易适应需求的变化或规模的扩大。
7.2 时间复杂度与空间复杂度分析:算法的“体检报告”
我们已经在数据结构课程中初步接触了时间复杂度与空间复杂度的概念,这里我们将再次强调并深入分析,因为这是算法学习的核心灵魂。
7.2.1 大O符号(Big O Notation):描述算法效率的“通用语言”
-
定义:大O符号是一种渐进表示法,用于描述算法的运行时间或所需空间与输入规模(n)之间的增长关系。它表示当输入规模
n趋于无穷大时,算法的增长趋势。 -
作用:它关注的是算法的效率级别,而不是精确的运行时间。它让我们能够比较不同算法的优劣,而与具体的硬件、编程语言无关。
-
忽略常数和低阶项:在计算大O时,我们会忽略常数、系数和低阶项,因为当
n足够大时,它们的影响可以忽略不计。-
例如:
O(2n + 5)->O(n)(当n非常大时,5可以忽略,2倍的n和n的增长趋势是一样的) -
例如:
O(3n^2 + 100n + 1000)->O(n^2)(当n非常大时,n^2的增长速度远超100n和1000)
-
7.2.1.1 常见复杂度级别
理解这些常见的复杂度级别,是评估算法效率的关键。从上到下,效率越来越低。
| 大O符号 | 描述 | 增长趋势 | 常见操作举例 | 形象比喻 |
|------------|--------------|-----------------------|------------------------------|---------------------------------|
| O(1) | 常数时间 | 极快,不随n变化 | 访问数组元素、哈希表查找 | 无论有多少本书,我知道第5页在哪,一步到位。|
| O(log n)| 对数时间 | 增长非常缓慢,高效 | 二分查找 | 每次排除一半,像查字典一样。 |
| O(n) | 线性时间 | 随n线性增长 | 遍历列表、无序数组查找 | 挨个检查,就像找房间号。 |
| O(n log n)| 线性对数时间| 相对高效,复杂排序 | 快速排序、归并排序、堆排序 | “效率较高”的排序。 |
| O(n²) | 平方时间 | 增长较快,效率低 | 冒泡排序、选择排序、两层嵌套循环| 两次遍历,像从一群人中找两两组合。|
| O(2ⁿ) | 指数时间 | 增长极快,效率极低 | 递归算法(如斐波那契数列未优化)| 每增加一个数据,计算量翻倍,任务量像滚雪球。 |
| O(n!) | 阶乘时间 | 增长恐怖,基本不可用 | 旅行商问题(暴力) | 计算所有可能排列组合,天文学数字。 |
7.2.2 时间复杂度分析:衡量算法的“速度”
-
定义:指算法执行所消耗的时间,通常用基本操作的执行次数来表示。我们关注的是算法随着输入规模
n的增加,其运行时间变化的趋势。 -
分析思路:
-
识别基本操作:找出算法中最耗时的操作(如比较、赋值、算术运算、数组访问等)。
-
统计操作次数:根据输入规模
n,计算基本操作的执行总次数。 -
使用大O符号表示:忽略常数和低阶项,得出最终的大O表示。
-
-
举例:
-
遍历列表(查找元素):
def find_element(arr, target): # 假设arr有n个元素 for x in arr: # 循环会执行n次 if x == target: # 每次循环进行一次比较操作 return True return False-
基本操作:
x == target(比较),最坏情况下会执行n次。 -
时间复杂度:O(n)。
-
-
嵌套循环(矩阵乘法):
def matrix_multiply(matrix_a, matrix_b, n): # 假设是n x n矩阵相乘 result_matrix = [[0 for _ in range(n)] for _ in range(n)] # O(n^2) 初始化 for i in range(n): # 外层循环n次 for j in range(n): # 中层循环n次 for k in range(n): # 内层循环n次 result_matrix[i][j] += matrix_a[i][k] * matrix_b[k][j] # 基本操作,执行n*n*n次 return result_matrix-
基本操作:
+=和*,执行了n * n * n = n^3次。 -
时间复杂度:O(n^3)。
-
-
递归函数:
-
例如,未优化的斐波那契数列:
fib(n) = fib(n-1) + fib(n-2)。其时间复杂度为O(2^n)。因为每次调用都会分裂成两个子调用,形成一棵指数增长的调用树。 -
分析递归算法的时间复杂度通常需要建立递推关系式,并用递归树或主定理(Master Theorem)等方法求解。
-
-
7.2.3 空间复杂度(Space Complexity):衡量算法的“内存消耗”
-
定义:指算法执行所消耗的存储空间,也通常用大O符号表示,描述算法在运行时额外占用的空间(不包括输入本身占用的空间)与输入规模
n的增长关系。 -
分析思路:关注算法执行过程中所使用的辅助空间,如:
-
额外的数据结构(数组、栈、队列、哈希表等)。
-
递归函数调用栈的深度(递归每调用一次,都会在栈上分配一块空间)。
-
-
举例:
-
冒泡排序:只需要常数个额外变量(用于交换元素)。
- 空间复杂度:O(1)。
-
归并排序:需要一个与输入数组大小相同的临时数组来合并已排序的子序列。
- 空间复杂度:O(n)。
-
递归函数:
-
计算阶乘的递归函数:
factorial(n) = n * factorial(n-1)。递归深度为n。 -
空间复杂度:O(n)(主要来自递归调用栈)。
-
-
最好/最坏/平均复杂度:
-
最好情况(Best Case):算法在最理想输入下的表现。例如,在已排序数组中查找第一个元素,线性查找是O(1)。
-
最坏情况(Worst Case):算法在最不利输入下的表现。这是我们通常关注的,因为它代表了算法性能的上限,确保即使在最极端情况下,算法也能在可接受的时间内完成。例如,在无序数组中查找一个不存在的元素,线性查找是O(n)。
-
平均情况(Average Case):所有可能输入的加权平均。这通常最能反映算法的真实性能,但分析起来也最复杂。例如,快速排序的平均时间复杂度是O(n log n),但最坏是O(n^2)。
同学们,复杂度分析是算法学习的核心灵魂。掌握它,你就能像专业的工程师一样,在编写代码之前就预判其性能瓶颈,并选择最合适的算法和数据结构。这是你在面试中脱颖而出,以及在实际项目中做出正确技术选型的重要依据。
好的,同学们,我们继续算法设计与分析的学习。前一节我们详细探讨了算法的基础概念,以及衡量算法效率的“火眼金睛”——时间复杂度和空间复杂度分析。现在,我们要把这些分析工具应用于最常见也最基础的一类算法——排序算法和查找(搜索)算法。
排序和查找是计算机科学中最基本、最频繁执行的操作。掌握不同的排序和查找算法,理解它们的效率差异和适用场景,是编写高效程序的必备技能。
三、排序算法:让数据“井然有序”
排序是将一组数据按照某种规则(如升序或降序)重新排列的过程。不同的排序算法在时间、空间复杂度和稳定性方面有所差异。
稳定性(Stability):指如果待排序的序列中存在值相等的元素,经过排序后,这些相等元素之间的相对次序保持不变,则称该排序算法是稳定的。
- 例子:如果班级里有两个学生都考了90分,小明在前,小红在后。如果排序后小明仍在小红之前,那么这个排序算法是稳定的。
3.1 冒泡排序(Bubble Sort):简单的“气泡上浮”
-
原理:重复遍历待排序的列表,比较相邻的两个元素,如果顺序错误就交换它们。每一轮遍历,都能将当前未排序部分的最大(或最小)元素“冒泡”到正确的位置。
-
时间复杂度:
-
最好情况:O(n) (列表已经有序,只需遍历一轮确认)
-
最坏/平均情况:O(n²) (需要进行n-1轮遍历,每轮比较和交换次数逐次减少)
-
-
空间复杂度:O(1) (只需少量额外空间进行元素交换)
-
稳定性:稳定 (只有当相邻元素顺序错误时才交换,相等元素不会交换位置)
-
比喻:就像水底的气泡,大的气泡会逐渐上浮。
def bubble_sort(arr):
n = len(arr)
# 外层循环控制遍历轮数,每轮将一个最大元素放到末尾
for i in range(n - 1): # n-1轮就足够
swapped = False # 优化:如果一轮遍历没有发生任何交换,说明列表已经有序
# 内层循环比较相邻元素,将最大值“冒泡”到正确位置
for j in range(n - 1 - i): # 每一轮都少比较已排序的元素
if arr[j] > arr[j + 1]:
# 交换元素
arr[j], arr[j + 1] = arr[j + 1], arr[j]
swapped = True
if not swapped: # 如果没有交换,提前退出
break
return arr
# 示例
my_list = [64, 34, 25, 12, 22, 11, 90]
print(f"冒泡排序前: {my_list}")
bubble_sort(my_list)
print(f"冒泡排序后: {my_list}") # [11, 12, 22, 25, 34, 64, 90]
3.2 选择排序(Selection Sort):每次选择最小的
-
原理:每一轮遍历,从未排序的区间中找到最小(或最大)的元素,然后将其放到已排序区间的末尾(即正确的位置)。
-
时间复杂度:
- 最好/最坏/平均情况:O(n²) (无论如何都需要嵌套循环,内层循环总是遍历到末尾找到最小元素)
-
空间复杂度:O(1)
-
稳定性:不稳定 (因为它可能将后面的较小元素交换到前面,从而改变相等元素的相对次序)
- 例子:
[5, 8, 5, 2, 9]。第一个5和第二个5,如果2被选择后交换到第一个5的位置,那么第二个5就会比第一个5更靠前,相对次序发生改变。
- 例子:
-
比喻:在扑克牌中,从手中找到最小的牌,放到最左边,再从剩下的牌中找最小的。
def selection_sort(arr):
n = len(arr)
# 外层循环控制已排序区间的边界
for i in range(n - 1):
min_idx = i # 假设当前未排序区间的第一个元素是最小的
# 内层循环从剩余未排序的元素中找到真正的最小元素
for j in range(i + 1, n):
if arr[j] < arr[min_idx]:
min_idx = j
# 将找到的最小元素与未排序区间的第一个元素交换
arr[i], arr[min_idx] = arr[min_idx], arr[i]
return arr
# 示例
my_list = [64, 34, 25, 12, 22, 11, 90]
print(f"选择排序前: {my_list}")
selection_sort(my_list)
print(f"选择排序后: {my_list}") # [11, 12, 22, 25, 34, 64, 90]
3.3 插入排序(Insertion Sort):像插扑克牌一样
-
原理:将列表分为已排序区和未排序区。每次从未排序区取出一个元素,将其插入到已排序区中的正确位置。
-
时间复杂度:
-
最好情况:O(n) (列表几乎有序,只需少量移动)
-
最坏/平均情况:O(n²)
-
-
空间复杂度:O(1)
-
稳定性:稳定 (在插入时,遇到与待插入元素相等的元素,新的元素插入到其后,保持相对次序)
-
比喻:你在打扑克牌时,每摸一张牌,都会把它插到你手中已排好序的牌的正确位置。
def insertion_sort(arr):
n = len(arr)
# 从第二个元素开始遍历(第一个元素默认已排序)
for i in range(1, n):
key = arr[i] # 待插入的元素
j = i - 1 # 已排序区间的最后一个元素索引
# 在已排序区间从右向左查找插入位置
# 如果当前已排序元素大于待插入元素,则向右移动
while j >= 0 and arr[j] > key:
arr[j + 1] = arr[j]
j -= 1
# 找到正确位置,插入待插入元素
arr[j + 1] = key
return arr
# 示例
my_list = [64, 34, 25, 12, 22, 11, 90]
print(f"插入排序前: {my_list}")
insertion_sort(my_list)
print(f"插入排序后: {my_list}") # [11, 12, 22, 25, 34, 64, 90]
3.4 快速排序(Quick Sort):分而治之,速度之王
-
原理:一种**分治(Divide and Conquer)**算法。
-
从列表中选择一个元素作为“基准值(Pivot)”。
-
将列表分区(Partition):所有比基准值小的元素移到基准值左边,所有比基准值大的元素移到基准值右边。
-
对基准值左右两边的子列表递归地重复上述步骤。
-
-
时间复杂度:
-
最好/平均情况:O(n log n) (非常高效)
-
最坏情况:O(n²) (当选择的基准值总是最大或最小元素时,退化为链表,如已排序列表,但可以通过随机选择基准值来避免)
-
-
空间复杂度:O(log n) (递归调用栈的深度,最坏情况O(n))
-
稳定性:不稳定 (分区过程可能改变相等元素的相对次序)
-
比喻:你有一大堆乱七八糟的书,挑一本作为标准(基准值),比它轻的放左边,比它重的放右边。然后分别对左右两堆书重复这个过程。
def quick_sort(arr):
if len(arr) <= 1:
return arr
pivot = arr[len(arr) // 2] # 选择中间元素作为基准值 (也可以随机选择)
# 列表推导式实现分区
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot] # 处理重复元素
right = [x for x in arr if x > pivot]
return quick_sort(left) + middle + quick_sort(right)
# 示例
my_list = [64, 34, 25, 12, 22, 11, 90, 25] # 加入重复元素
print(f"快速排序前: {my_list}")
sorted_list = quick_sort(my_list) # 快排通常返回新列表
print(f"快速排序后: {sorted_list}") # [11, 12, 22, 25, 25, 34, 64, 90]
3.5 归并排序(Merge Sort):分而治之,合并有序
-
原理:也是一种**分治(Divide and Conquer)**算法。
-
分解(Divide):将列表递归地分成两半,直到每个子列表只有一个元素(单元素列表自然有序)。
-
合并(Conquer/Merge):将有序的子列表逐对合并,形成更大的有序列表,直到所有子列表合并成一个完整的有序列表。
-
-
时间复杂度:
- 最好/最坏/平均情况:O(n log n) (非常稳定和高效)
-
空间复杂度:O(n) (需要一个额外的临时数组来存放合并结果)
-
稳定性:稳定 (合并过程可以保证相等元素的相对次序)
-
比喻:把一大叠乱序的牌分成两半,再把每半继续分,直到只剩一张牌。然后把两堆有序的牌合并成一堆更大的有序牌。
def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid]) # 递归分解左半部分
right = merge_sort(arr[mid:]) # 递归分解右半部分
# 合并两个有序的子列表
res = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] < right[j]:
res.append(left[i])
i += 1
else:
res.append(right[j])
j += 1
# 将剩余元素添加到结果列表
res.extend(left[i:])
res.extend(right[j:])
return res
# 示例
my_list = [64, 34, 25, 12, 22, 11, 90]
print(f"归并排序前: {my_list}")
sorted_list = merge_sort(my_list) # 归并排序通常返回新列表
print(f"归并排序后: {sorted_list}") # [11, 12, 22, 25, 34, 64, 90]
3.6 堆排序(Heap Sort):利用堆的特性
-
原理:利用堆(通常是最大堆)的数据结构特性。
-
建堆:将待排序列表构造成一个最大堆(或最小堆)。此时,堆顶元素就是最大(或最小)值。
-
堆排序:将堆顶元素与堆的最后一个元素交换。然后将堆的大小减1,并对剩余的堆进行“堆化”(Heapify),恢复堆的特性。重复此步骤,直到堆中只剩一个元素。
-
-
时间复杂度:最好/最坏/平均情况:O(n log n)
-
空间复杂度:O(1) (原地排序,不需额外数组)
-
稳定性:不稳定 (交换堆顶和末尾元素时可能破坏相等元素的相对次序)
-
比喻:你把所有积木堆成一个“金字塔”(堆),最大的在顶端。每次把顶端的拿出来,放在一边,再调整剩下的积木,让第二大的到顶端。
import heapq
def heap_sort(arr):
# 1. 建堆:将列表转换为最小堆 (Python的heapq默认是最小堆)
# 如果要实现最大堆,可以存入负数,或自定义比较函数
heapq.heapify(arr)
sorted_arr = []
# 2. 堆排序:依次弹出堆顶元素 (最小元素)
while arr:
sorted_arr.append(heapq.heappop(arr))
return sorted_arr
# 示例
my_list = [64, 34, 25, 12, 22, 11, 90]
print(f"堆排序前: {my_list}")
sorted_list = heap_sort(my_list)
print(f"堆排序后: {sorted_list}") # [11, 12, 22, 25, 34, 64, 90]
四、查找(搜索)算法:在数据中“寻找宝藏”
查找算法是在数据集合中寻找特定元素的过程。
4.1 线性查找(Linear Search)/顺序查找
-
原理:从数据集合的第一个元素开始,依次遍历每一个元素,直到找到目标元素或遍历完所有元素。
-
适用场景:数据量小、数据无序的集合。
-
时间复杂度:
-
最好情况:O(1) (第一个就是目标)
-
最坏/平均情况:O(n)
-
-
空间复杂度:O(1)
def linear_search(arr, target):
for i in range(len(arr)):
if arr[i] == target:
return i # 返回索引
return -1 # 未找到
# 示例
my_list = [10, 5, 20, 15, 25]
print(f"线性查找 20: {linear_search(my_list, 20)}") # 2
print(f"线性查找 30: {linear_search(my_list, 30)}") # -1
4.2 二分查找(Binary Search)
-
原理:适用于有序数组。每次查找都通过比较中间元素,将查找范围缩小一半。
-
时间复杂度:
- 最好/最坏/平均情况:O(log n) (非常高效)
-
空间复杂度:O(1) (迭代实现) 或 O(log n) (递归实现,递归栈深度)
-
比喻:你在一本字典里找一个单词,不会从第一页开始翻,而是先翻到中间,根据字母顺序判断是在前一半还是后一半,然后重复这个过程。
def binary_search(arr, target):
left, right = 0, len(arr) - 1 # 定义查找范围的左右边界
while left <= right: # 当左边界小于或等于右边界时,循环继续
mid = left + (right - left) // 2 # 计算中间索引,防止溢出
if arr[mid] == target:
return mid # 找到目标,返回索引
elif arr[mid] < target:
left = mid + 1 # 目标在右半部分,更新左边界
else: # arr[mid] > target
right = mid - 1 # 目标在左半部分,更新右边界
return -1 # 未找到
# 示例 (必须是排序过的列表)
my_list = [11, 12, 22, 25, 34, 64, 90]
print(f"二分查找 25: {binary_search(my_list, 25)}") # 3
print(f"二分查找 100: {binary_search(my_list, 100)}") # -1
4.3 哈希查找(Hash Search)
-
原理:利用哈希表(Hash Table)的数据结构特性,通过哈希函数将键直接映射到存储位置。
-
适用场景:需要快速进行精确查找、插入、删除操作的无序集合、去重、统计。
-
时间复杂度:
-
平均情况:O(1)
-
最坏情况:O(n) (当哈希冲突严重时,退化为线性查找)
-
-
空间复杂度:O(n) (需要额外的空间来存储哈希表)
-
比喻:你有一个可以直接通过“名字”(键)就能找到对应电话号码(值)的电话簿。
# Python字典就是哈希查找的实现
my_dict = {"apple": 1, "banana": 2, "cherry": 3}
print(f"哈希查找 'banana': {my_dict.get('banana')}") # 2
print(f"哈希查找 'grape': {my_dict.get('grape', '未找到')}") # 未找到
同学们,排序和查找算法是算法的入门,但它们是所有复杂系统不可或缺的基础。理解它们的原理、效率和适用性,能让你在日常编程中做出更明智的选择。
好的,同学们,我们继续算法设计与分析的学习。前一节我们详细探讨了各种排序算法和查找算法,掌握了它们在不同场景下的效率表现。现在,我们将进入更抽象、更具智慧的算法设计思想层面:递归与分治、动态规划和贪心算法。
这些设计思想是解决复杂问题的“利器”,它们不针对某个特定问题,而是一种通用的问题解决范式,能够帮助你将大问题拆解为小问题,或从局部最优解推导出全局最优解。
五、递归与分治:大问题分解为小问题
5.1 递归(Recursion):自己调用自己
-
概念:递归是指函数(或过程)直接或间接地调用自身。
-
构成要素:一个有效的递归函数必须包含两个部分:
-
递归出口/基线条件(Base Case):这是递归终止的条件。当满足这个条件时,函数不再调用自身,而是直接返回一个确定的结果。没有递归出口会导致无限递归(栈溢出)。
-
递归调用/递归步骤(Recursive Step):在问题规模缩小后,函数调用自身来解决更小规模的子问题。
-
-
适用场景:递归通常适用于问题本身可以用相同方式的子问题来定义的情况,例如:
-
树的遍历、图的深度优先搜索。
-
分形问题、一些数学序列(如阶乘、斐波那契数列)。
-
分治算法的实现。
-
-
老师提示:虽然递归代码通常简洁优美,但它会占用额外的栈空间(每调用一次函数,就会在调用栈中压入一次),当递归深度过大时,可能导致栈溢出(Stack Overflow)。
# 示例1:计算阶乘 (Factorial)
# n! = n * (n-1)!
# 0! = 1 (基线条件)
def factorial(n):
if n == 0: # 递归出口/基线条件
return 1
else:
return n * factorial(n - 1) # 递归调用,问题规模n缩小为n-1
print(f"5的阶乘是: {factorial(5)}") # 120 (5 * 4 * 3 * 2 * 1)
# 示例2:斐波那契数列 (Fibonacci Sequence) - 未优化版
# F(n) = F(n-1) + F(n-2)
# F(0) = 0, F(1) = 1 (基线条件)
def fibonacci(n):
if n <= 1: # 递归出口
return n
else:
return fibonacci(n - 1) + fibonacci(n - 2) # 递归调用
print(f"斐波那契数列第7项是: {fibonacci(7)}") # 13
# 老师提示:此斐波那契函数存在大量重复计算,效率非常低,时间复杂度是O(2^n)。
# 这正是后面要讲的“动态规划”要解决的问题。
5.2 分治法(Divide and Conquer):“各个击破”的策略
-
概念:分治法是一种重要的算法设计范式,它将一个难以直接解决的大问题,分解(Divide)成若干个规模较小、相互独立、与原问题形式相同的子问题。然后对这些子问题进行递归求解(Conquer),最后将子问题的解**合并(Combine)**起来,得到原问题的解。
-
三个步骤:
-
分解(Divide):将原问题分解成若干个子问题。
-
解决(Conquer):递归地解决这些子问题。如果子问题足够小(达到基线条件),则直接解决。
-
合并(Combine):将子问题的解合并成原问题的解。
-
-
适用场景:当一个问题可以被分解为独立且结构相同的子问题时。
-
典型应用:
-
归并排序(Merge Sort):分解(分半) -> 排序(递归) -> 合并(合并有序子数组)。
-
快速排序(Quick Sort):分解(分区) -> 排序(递归) -> 合并(无需显式合并,分区已完成大部分工作)。
-
最大子数组和问题:在一个数组中找到和最大的连续子数组。
-
矩阵乘法:Strassen算法。
-
六、动态规划(Dynamic Programming, DP):“记忆化”的优化策略
6.1 概念与思想:避免重复计算的“智慧”
-
概念:动态规划是一种将复杂问题分解成重叠子问题(Overlapping Subproblems)和最优子结构(Optimal Substructure)的优化技术。它通过存储(记忆化)已经计算过的子问题的解,避免重复计算,从而大幅提高效率。
-
核心思想:
-
最优子结构:原问题的最优解可以通过其子问题的最优解来构造。
-
重叠子问题:在递归求解过程中,同一个子问题会被多次计算。动态规划通过记忆化(或自底向上填充表格)来解决这个问题。
-
-
与分治的区别:
-
分治:子问题是独立的,可以并行处理。
-
动态规划:子问题之间有重叠,需要记录子问题的解以避免重复计算。
-
-
比喻:你解一道大难题。分治法是把大难题分成几个小难题,然后分别解决。动态规划是发现这些小难题里有很多是重复的,于是你把第一个重复的小难题的答案记住,下次遇到一样的就直接用,不用再算一遍。
6.2 常见问题类型
动态规划适用于许多经典问题,包括:
-
斐波那契数列(优化递归)
-
背包问题(如0-1背包、完全背包、多重背包)
-
最长公共子序列(LCS)
-
最短路径问题(如Bellman-Ford、Floyd-Warshall,虽然Dijkstra也算一种特殊DP)
-
矩阵链乘法、编辑距离、区间DP等。
6.3 实现方式
-
自顶向下(Top-Down) / 记忆化搜索(Memoization):
-
思路:从原问题开始,递归地分解为子问题。在计算子问题时,如果之前已经计算过,就直接从存储中获取结果;否则,计算并存储结果。
-
特点:代码结构与递归相似,更符合人类思维。
-
-
自底向上(Bottom-Up) / 迭代(Tabulation):
-
思路:从最简单的子问题开始计算,逐步构建出更大规模子问题的解,直到得到原问题的解。通常使用一个表格(数组)来存储子问题的解。
-
特点:通常使用循环迭代实现,避免了递归栈的开销,性能更好。
-
6.4 示例:斐波那契数列(动态规划优化)
-
未优化的递归(时间O(2^n),空间O(n)):
def fib_recursive(n): if n <= 1: return n return fib_recursive(n - 1) + fib_recursive(n - 2) -
记忆化递归(自顶向下,时间O(n),空间O(n)):
memo = {} # 用于存储已经计算过的斐波那契数 def fib_memo(n): if n <= 1: return n if n in memo: # 如果已经计算过,直接返回 return memo[n] # 计算并存储结果 memo[n] = fib_memo(n - 1) + fib_memo(n - 2) return memo[n] print(f"记忆化斐波那契数列第7项是: {fib_memo(7)}") # 13 print(f"记忆化斐波那契数列第50项是: {fib_memo(50)}") # 12586269025 (快速计算) -
动态规划(自底向上,时间O(n),空间O(n),可优化为O(1)空间):
def fib_dp(n): if n <= 1: return n # dp数组存储子问题的解,dp[i]表示第i项斐波那契数 dp = [0] * (n + 1) dp[0] = 0 dp[1] = 1 for i in range(2, n + 1): dp[i] = dp[i - 1] + dp[i - 2] return dp[n] print(f"动态规划斐波那契数列第7项是: {fib_dp(7)}") # 13 print(f"动态规划斐波那契数列第50项是: {fib_dp(50)}") # 12586269025- 空间优化:斐波那契数列的计算只依赖前两项,所以可以将空间复杂度优化到O(1),只需两个变量存储前两项。
6.5 经典背包问题(0-1背包):选择与价值
-
问题:给定一个背包的容量
W和一系列物品,每个物品有自己的重量w[i]和价值v[i]。每个物品只能选择装入或不装入(0-1)。目标是选择物品装入背包,使得总价值最大,且总重量不超过背包容量。 -
状态定义:
dp[i][j]表示在前i个物品中选择,背包容量为j时能获得的最大价值。 -
状态转移方程:
-
如果第
i个物品的重量w[i-1]大于当前背包容量j,则不能装入第i个物品:dp[i][j] = dp[i-1][j] -
如果能装入:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i-1]] + v[i-1])-
dp[i-1][j]:不装入第i个物品时的最大价值。 -
dp[i-1][j-w[i-1]] + v[i-1]:装入第i个物品时的最大价值。
-
-
-
Python实现:
def knapsack_01(weights, values, W):
n = len(weights) # 物品数量
# dp[i][j] 表示前i个物品,背包容量为j时能获得的最大价值
# dp数组大小 (n+1) x (W+1),因为要考虑0个物品和0容量的情况
dp = [[0] * (W + 1) for _ in range(n + 1)]
# 填充dp表
for i in range(1, n + 1): # 遍历物品 (从1到n个物品)
current_weight = weights[i - 1] # 当前物品的重量 (因为索引从0开始)
current_value = values[i - 1] # 当前物品的价值
for w_capacity in range(W + 1): # 遍历当前背包容量 (从0到W)
if w_capacity >= current_weight: # 如果当前背包容量能装下当前物品
# 选择装入或不装入当前物品,取最大价值
# 不装入:dp[i-1][w_capacity] (前i-1个物品,容量仍为w_capacity)
# 装入:dp[i-1][w_capacity - current_weight] + current_value (前i-1个物品,容量减去当前物品重量,再加上当前物品价值)
dp[i][w_capacity] = max(dp[i - 1][w_capacity],
dp[i - 1][w_capacity - current_weight] + current_value)
else: # 当前背包容量装不下当前物品,只能选择不装入
dp[i][w_capacity] = dp[i - 1][w_capacity]
# 最终结果在dp[n][W]
return dp[n][W]
# 示例
weights = [2, 1, 3, 2] # 物品重量
values = [3, 2, 4, 2] # 物品价值
W = 5 # 背包容量
max_value = knapsack_01(weights, values, W)
print(f"0-1背包问题,最大价值: {max_value}") # 7 (选择物品1(3), 2(2), 4(2), 总重2+1+2=5, 总价值3+2+2=7)
- 老师提示:0-1背包问题还可以通过将二维dp数组优化为一维dp数组来实现O(W)的空间复杂度,但需要注意遍历方向。
七、贪心算法(Greedy Algorithm):“目光短浅”但“高效”
7.1 基本思想:每一步都做到最好
-
概念:贪心算法在每一步都做出在当前看起来是最优(局部最优)的选择。它期望通过一系列的局部最优选择,最终能够得到全局最优解。
-
特点:
-
目光短浅:只考虑当前一步的最优,不考虑后续步骤的后果。
-
没有“后悔药”:一旦做出选择,就不能撤销或修改。
-
-
适用场景:贪心算法只有在特定类型的问题中才能得到全局最优解。这通常需要问题具备贪心选择性质(Greedy Choice Property)和最优子结构性质(Optimal Substructure),但与动态规划不同,其子问题是不重叠的。
-
贪心选择性质:一个全局最优解可以通过局部最优(贪心)选择来达到。
-
最优子结构性质:原问题的最优解包含了子问题的最优解。
-
7.2 应用实例
-
找零钱问题:在纸币面额固定的情况下(如1, 5, 10, 20, 50, 100元),总是优先使用面额最大的纸币找零。这是贪心算法的经典应用。
-
例外:如果面额不合理,如1, 3, 4元,找6元:
-
贪心:4元+1元+1元 (3张)
-
最优:3元+3元 (2张)
-
所以贪心算法不总是对的。
-
-
-
最小生成树(Minimum Spanning Tree, MST):
- Prim算法和Kruskal算法都是基于贪心策略。
-
活动选择/区间调度问题:给定一系列活动,每个活动有开始时间和结束时间,选择最多数量的活动,使得它们互不重叠。
-
霍夫曼编码(Huffman Coding):一种用于数据压缩的算法。
7.3 示例:区间覆盖(活动选择问题)
-
问题:给定一组会议区间(开始时间,结束时间),找出能够参加的最大不重叠会议数量。
-
贪心策略:每次选择结束时间最早的那个会议。
- 理由:结束时间最早的会议,能够为后续会议留下最大的空间。
-
Python实现:
def max_non_overlapping_intervals(intervals):
"""
计算最大不重叠区间数量。
intervals: 列表,每个元素是一个元组 (start, end)
"""
if not intervals:
return 0
# 贪心策略:按结束时间排序
intervals.sort(key=lambda x: x[1])
count = 1 # 至少可以参加一个会议
end_time = intervals[0][1] # 当前已参加会议的结束时间
# 遍历排序后的会议
for i in range(1, len(intervals)):
current_start = intervals[i][0]
current_end = intervals[i][1]
# 如果当前会议的开始时间 >= 上一个已参加会议的结束时间
if current_start >= end_time:
count += 1
end_time = current_end # 更新结束时间
return count
# 示例
intervals1 = [(1, 3), (2, 6), (8, 10), (15, 18)] # 参加 (1,3), (8,10), (15,18),共3个
intervals2 = [(1, 4), (2, 5), (6, 7)] # 参加 (1,4), (6,7) 或 (2,5), (6,7),共2个
intervals3 = [(1, 2), (2, 3), (3, 4), (1, 3)] # 参加 (1,2), (2,3), (3,4),共3个
print(f"最大不重叠区间数量 {intervals1}: {max_non_overlapping_intervals(intervals1)}") # 3
print(f"最大不重叠区间数量 {intervals2}: {max_non_overlapping_intervals(intervals2)}") # 2
print(f"最大不重叠区间数量 {intervals3}: {max_non_overlapping_intervals(intervals3)}") # 3
同学们,递归与分治、动态规划、贪心算法是解决复杂问题的三大核心思想。它们各有特点,适用于不同类型的问题。掌握它们,你就能从更宏观的层面思考问题,并设计出高效的解决方案。
好的,同学们,我们继续算法设计与分析的学习。前一节我们详细探讨了递归与分治、动态规划和贪心算法这三大算法设计思想。现在,我们将进入更具挑战性但应用极其广泛的领域——图算法,并总结算法设计的通用思想与优化策略。
大家还记得数据结构中的图吗?它能表示复杂的多对多关系。而图算法,就是如何在这些复杂的网络中找到路径、发现连接、进行排序或分配资源。它们在社交网络、地图导航、推荐系统、物流规划等众多领域都有着核心应用。
八、图算法:网络世界的“导航员”与“分析师”
图算法是处理图数据结构中的各种问题的方法。
8.1 深度优先搜索(DFS):一根筋“走到底”
-
原理:从图中的一个顶点开始,沿着一条路径尽可能深地探索,直到不能再深入为止。然后回溯到上一个顶点,尝试另一条未访问过的路径。
-
实现方式:
-
递归(Recursion):最直观的方式,利用函数调用栈。
-
栈(Stack):手动维护一个栈来模拟递归过程。
-
-
应用场景:
-
遍历图或树:访问所有节点。
-
寻找连通分量:找出图中相互连接的子图。
-
判断图中是否存在环。
-
拓扑排序:对有向无环图(DAG)进行线性排序。
-
迷宫寻路:寻找一条路径(不一定是最近的)。
-
回溯算法:许多组合问题(如全排列、数独求解)的通用框架。
-
-
时间复杂度:O(V + E),V是顶点数,E是边数。
# 示例图 (邻接表表示)
graph_dfs_bfs = {
'A': ['B', 'C'],
'B': ['A', 'D', 'E'],
'C': ['A', 'F'],
'D': ['B'],
'E': ['B', 'F'],
'F': ['C', 'E']
}
print("--- 深度优先搜索 (DFS) ---")
def dfs_example(graph, start_node, visited=None):
if visited is None:
visited = set() # 用集合记录已访问节点,防止重复访问和死循环
visited.add(start_node)
print(start_node, end=" ") # 访问当前节点
# 遍历当前节点的所有邻居
for neighbor in graph[start_node]:
if neighbor not in visited: # 如果邻居未被访问,则递归访问
dfs_example(graph, neighbor, visited)
dfs_example(graph_dfs_bfs, 'A') # 输出: A B D E F C (具体顺序取决于邻接表存储顺序)
print("\n")
8.2 广度优先搜索(BFS):一层层“扩散”
-
原理:从图中的一个顶点开始,先访问其所有直接邻居,然后访问这些邻居的所有未访问过的邻居,依此类推,逐层向外扩展。
-
实现方式:通常使用**队列(Queue)**来辅助实现。
-
应用场景:
-
遍历图或树:访问所有节点(通常用于层序遍历)。
-
无权图中的最短路径:BFS能够找到从起点到所有其他可达顶点的最短路径(边数最少)。
-
社交网络关系层级:找出“六度人脉”的距离。
-
网络爬虫:从一个页面开始抓取所有链接的页面。
-
查找最近的资源:如在游戏中查找最近的敌人。
-
-
时间复杂度:O(V + E)。
from collections import deque # 导入队列
print("--- 广度优先搜索 (BFS) ---")
def bfs_example(graph, start_node):
visited = set() # 记录已访问节点
queue = deque([start_node]) # 初始化队列,放入起始节点
visited.add(start_node) # 标记起始节点已访问
while queue:
node = queue.popleft() # 弹出队首节点
print(node, end=" ") # 访问当前节点
# 遍历当前节点的所有邻居
for neighbor in graph[node]:
if neighbor not in visited: # 如果邻居未被访问,则入队并标记
visited.add(neighbor)
queue.append(neighbor)
bfs_example(graph_dfs_bfs, 'A') # 输出: A B C D E F (严格按层级和邻接表顺序)
print("\n\n")
8.3 最短路径算法:从A到B的最优路径
最短路径算法用于在带权图中找到两个节点之间路径权重之和最小的路径。
-
Dijkstra算法(迪克斯特拉算法):
-
用途:用于解决单源最短路径问题,即从一个给定源点到图中所有其他顶点的最短路径。
-
限制:不能处理负权边(边的权重不能为负数)。
-
实现:通常使用**优先队列(堆)**来优化,每次从待处理节点中选择距离源点最近的节点。
-
时间复杂度:O(E log V) 或 O(E + V log V) (使用优先队列优化)。
-
应用:地图导航(路径规划)、网络路由协议、交通规划。
-
import heapq # 导入堆模块
# 示例图 (邻接表表示,带权重)
# 格式: {节点: [(邻居, 权重), ...]}
graph_dijkstra = {
'A': [('B', 1), ('C', 4)],
'B': [('A', 1), ('C', 2), ('D', 5)],
'C': [('A', 4), ('B', 2), ('D', 1)],
'D': [('B', 5), ('C', 1)]
}
print("--- Dijkstra 最短路径算法 ---")
def dijkstra(graph, start_node):
# 初始化距离字典,所有点到源点距离设为无穷大,源点为0
distances = {node: float('inf') for node in graph}
distances[start_node] = 0
# 优先队列 (存放元组: (距离, 节点))
# 优先队列会自动按元组的第一个元素(距离)进行排序
priority_queue = [(0, start_node)] # (距离源点的距离, 节点)
while priority_queue:
current_distance, current_node = heapq.heappop(priority_queue)
# 如果当前节点已被处理过(即找到了更短的路径),则跳过
if current_distance > distances[current_node]:
continue
# 遍历当前节点的所有邻居
for neighbor, weight in graph[current_node]:
distance = current_distance + weight
# 如果通过当前节点到达邻居的路径更短
if distance < distances[neighbor]:
distances[neighbor] = distance # 更新最短距离
heapq.heappush(priority_queue, (distance, neighbor)) # 将邻居加入优先队列
return distances
# 示例
start_node = 'A'
shortest_paths = dijkstra(graph_dijkstra, start_node)
print(f"从节点 '{start_node}' 到其他节点的最短路径: {shortest_paths}")
# 期望输出: {'A': 0, 'B': 1, 'C': 3, 'D': 4}
print("\n")
-
Floyd算法(弗洛伊德算法):
-
用途:解决所有顶点对之间的最短路径问题(多源最短路径)。
-
限制:可以处理负权边,但不能处理负权环。
-
实现:动态规划。
-
时间复杂度:O(V³)。
-
适用场景:稠密图。
-
8.4 最小生成树(Minimum Spanning Tree, MST)
-
问题:在一个连通的带权无向图中,找到一棵包含所有顶点的子图,使得这棵子图的边权之和最小,并且没有环。
-
应用:网络布线、交通网络规划、集群通信线路设计。
-
两种经典算法:
-
Kruskal算法(克鲁斯卡尔算法):
-
原理:从所有边中选择权重最小的边,如果加入这条边不形成环,就加入。重复此过程直到所有顶点都连通。
-
实现:通常使用并查集来判断是否形成环。
-
时间复杂度:O(E log E) 或 O(E log V)。
-
-
Prim算法(普里姆算法):
-
原理:从一个起始顶点开始,逐步将顶点加入到最小生成树中,每次选择连接树中顶点与树外顶点且权重最小的边。
-
实现:通常使用优先队列。
-
时间复杂度:O(E log V) 或 O(E + V log V)。
-
-
8.5 拓扑排序(Topological Sort)
-
问题:对**有向无环图(DAG, Directed Acyclic Graph)**的所有顶点进行线性排序,使得对于图中的任意一条有向边
u -> v,u在排序结果中都出现在v之前。 -
应用:项目管理(任务依赖)、编译系统(文件依赖)、课程先修关系、事件调度。
-
实现:
-
Kahn算法(基于入度):使用队列。
-
DFS算法:使用递归和栈。
-
-
老师提示:如果一个图存在环,则无法进行拓扑排序。
8.6 强连通分量(Strongly Connected Components, SCC)
-
问题:在有向图中,如果任意两个顶点
u和v之间都存在从u到v和从v到u的路径,则称这两个顶点强连通。一个有向图的强连通分量是其最大的强连通子图。 -
应用:社交网络中的兴趣小组、程序调用关系分析、密码学。
-
算法:Tarjan算法、Kosaraju算法。
九、算法设计思想与优化:从宏观到微观的智慧
掌握具体算法的同时,更重要的是理解它们背后的设计思想。
9.1 常见设计思想:解决问题的“套路”
-
分治(Divide and Conquer):
-
核心:分解、解决、合并。将大问题分解为独立子问题,递归求解,然后合并。
-
代表:快速排序、归并排序。
-
-
动态规划(Dynamic Programming):
-
核心:最优子结构、重叠子问题。通过记忆化或自底向上填充表格,避免重复计算。
-
代表:背包问题、最长公共子序列。
-
-
贪心(Greedy):
-
核心:每一步都做出局部最优选择,期望得到全局最优解。
-
代表:找零钱(特定面额)、Prim/Kruskal最小生成树。
-
-
回溯(Backtracking):
-
核心:在搜索解空间时,如果当前路径无法得到有效解,则退回一步,尝试其他路径。
-
代表:八皇后问题、全排列、数独求解、N-Queens。
-
-
分支限界(Branch and Bound):
-
核心:在搜索解空间树时,通过估计算法,剪掉那些不可能包含最优解的分支,减少搜索范围。
-
代表:旅行商问题(优化版)。
-
-
启发式算法(Heuristic Algorithm):
-
核心:不保证找到最优解,但在合理的时间内找到一个足够好的近似解。
-
代表:A*搜索算法(寻路)。
-
-
近似算法(Approximation Algorithm):
- 核心:对于NP-hard问题(目前无多项式时间解法的问题),在多项式时间内找到一个接近最优解的算法。
9.2 算法优化技巧:让算法“跑得更快,吃得更少”
-
空间换时间:
-
使用额外的内存空间来存储预计算结果,从而减少计算时间。
-
例子:哈希表(牺牲空间实现O(1)查找)、动态规划的dp表格、缓存(Caching)。
-
-
提前剪枝、预处理:
-
在搜索或计算过程中,如果发现当前路径不可能得到最优解,则立即停止,剪掉该分支。
-
对输入数据进行预先处理或排序,以便后续算法能更高效地运行。
-
-
减少嵌套循环层数:
- 多层嵌套循环通常是性能瓶颈,尝试将嵌套循环转换为哈希查找、排序或更巧妙的算法。
-
递归转迭代,动态规划表优化:
-
递归虽然简洁,但有栈溢出风险和函数调用开销。可以将递归算法(特别是尾递归)转换为迭代实现。
-
动态规划中的dp表格,有时可以进行空间优化(如斐波那契数列)。
-
-
并行计算、多线程/多进程/分布式:
- 将问题分解为可独立执行的子任务,在多核CPU或分布式集群上并行运行,提高吞吐量。
十、实践项目与经典问题:从理论到实战
10.1 经典算法题举例
-
排序算法实现与性能对比:亲手实现冒泡、选择、插入、归并、快速、堆排序,并用计时器比较它们在不同数据规模和初始状态下的运行时间,加深对时间复杂度的理解。
-
二分查找在有序数组/旋转数组中的应用:这是面试高频题,考察你对边界条件和逻辑的把握。
-
LRU缓存机制:使用双向链表和哈希表实现,锻炼数据结构组合应用。
-
背包问题、多重背包、完全背包:动态规划的经典变种。
-
最长递增子序列、最长公共子序列:动态规划的另一类经典问题。
-
字符串匹配:KMP算法、Rabin-Karp算法。
-
图的连通性、最短路径、拓扑排序:图算法是面试难点,也是工程应用重点。
-
回溯法解数独、八皇后、全排列:递归与回溯的经典应用。
10.2 实践项目:背包问题、最短路径的实现与对比
-
用Python实现0-1背包问题(动态规划):我们前面已经提供了代码。
-
用Python实现Dijkstra最短路径算法:我们前面也提供了代码。
-
对比递归、记忆化、迭代解法的效率:
-
以斐波那契数列为例,分别实现原始递归、记忆化递归、迭代动态规划三种版本。
-
使用
timeit模块或time模块的perf_counter()函数测量它们在计算大N值(如N=30, 40, 50)时的运行时间,直观感受复杂度级别的差异。
import time def fib_recursive_raw(n): # O(2^n) if n <= 1: return n return fib_recursive_raw(n-1) + fib_recursive_raw(n-2) memo_fib = {} def fib_recursive_memo(n): # O(n) if n <= 1: return n if n in memo_fib: return memo_fib[n] memo_fib[n] = fib_recursive_memo(n-1) + fib_recursive_memo(n-2) return memo_fib[n] def fib_dp_iterative(n): # O(n) 空间O(n) if n <= 1: return n dp = [0] * (n + 1) dp[0] = 0; dp[1] = 1 for i in range(2, n + 1): dp[i] = dp[i-1] + dp[i-2] return dp[n] def fib_dp_optimized_space(n): # O(n) 空间O(1) if n <= 1: return n a, b = 0, 1 for _ in range(2, n + 1): a, b = b, a + b return b print("--- 斐波那契数列效率对比 ---") for n_val in [20, 30, 35, 40]: # 40可能已经很慢了 print(f"\n计算 F({n_val}):") # raw recursive (可能非常慢,甚至卡死或栈溢出) # start = time.perf_counter() # res = fib_recursive_raw(n_val) # end = time.perf_counter() # print(f"原始递归耗时: {end - start:.4f}秒, 结果: {res}") # memoized recursive memo_fib.clear() # 清除缓存 start = time.perf_counter() res = fib_recursive_memo(n_val) end = time.perf_counter() print(f"记忆化递归耗时: {end - start:.4f}秒, 结果: {res}") # iterative DP (O(n) space) start = time.perf_counter() res = fib_dp_iterative(n_val) end = time.perf_counter() print(f"迭代DP(O(n)空间)耗时: {end - start:.4f}秒, 结果: {res}") # iterative DP (O(1) space) start = time.perf_counter() res = fib_dp_optimized_space(n_val) end = time.perf_counter() print(f"迭代DP(O(1)空间)耗时: {end - start:.4f}秒, 结果: {res}")- 用matplotlib展示算法的复杂度对比:对于不同时间复杂度的算法(如O(n), O(n log n), O(n^2)),可以生成不同N值下的运行时间(或操作次数),然后用Matplotlib画图,直观地展示不同曲线的增长趋势。
-
十一、与后续课程的逻辑衔接:算法是系统性能的“灵魂”
-
数据结构决定算法效率,算法提升系统性能:这是计算机科学的核心真理。理解它们是解决实际工程问题的关键。
-
Web后端与性能优化:
-
数据库索引(B+树)是数据结构,如何写出高效SQL查询(算法),直接影响后端性能。
-
缓存淘汰策略(如LRU)是数据结构和算法的结合。
-
API响应速度:取决于业务逻辑中使用的算法效率。
-
-
前端性能:即使是前端,大数据量渲染(如虚拟列表)、状态管理优化也涉及数据结构和算法思想。
-
架构优化:在设计大型分布式系统时,如何进行数据分片、路由、负载均衡,都离不开对算法和数据结构的深刻理解。
-
人工智能与数据科学:机器学习模型的训练和推理,底层都是大规模的线性代数运算和各种优化算法。
-
面试与工程开发的核心竞争力:算法能力是所有一线IT公司技术面试的重中之重,也是你在实际工程中解决复杂问题、优化系统性能的“杀手锏”。
十二、学习建议与资料拓展:持续挑战,成为高手
-
刷题实践:
-
LeetCode:继续在LeetCode上刷题,这是提高算法能力最有效的方法。从简单题开始,逐步挑战中等和困难题。
-
牛客网、力扣、LintCode:国内常用的刷题平台,同样提供丰富的题目和社区讨论。
-
-
经典教材:
-
《算法(第四版)》(Algorithms, 4th Edition, Sedgewick & Wayne):业界经典,讲解深入浅出。
-
《算法导论》(Introduction to Algorithms):计算机科学领域的圣经,非常全面和严谨,适合深入研究。
-
《编程之美》:微软面试题集,结合实际问题讲解算法。
-
-
在线课程:
-
MIT 6.006 (Introduction to Algorithms) 和 6.046 (Design and Analysis of Algorithms):麻省理工学院的免费公开课,质量极高。
-
极客时间、B站、慕课网等平台上有许多优质的算法课程,可以结合视频学习。
-
-
参与开源项目:在实际项目中应用算法,或者优化现有代码,将理论与实践结合。
十三、课后练习与思考:挑战你的算法思维
-
实现并对比排序算法:
-
用Python实现选择排序、插入排序、归并排序、快速排序和堆排序。
-
分别用不同规模的数据(如1000, 10000, 50000, 100000个随机数)测试它们的运行时间,并用你学过的数据结构(如列表)和
time.perf_counter()计时,直观感受不同时间复杂度的差异。
-
-
动态规划与贪心问题:
-
选择一个你感兴趣的动态规划问题(例如最长公共子序列、最小路径和、编辑距离),尝试自己实现它。
-
思考并选择一个可以用贪心算法解决的问题(例如活动选择),并证明其贪心选择性质。
-
-
图遍历与应用:
-
用DFS与BFS分别遍历给定的无向图和有向图。
-
尝试实现一个简单的无权图最短路径算法(BFS),或Dijkstra算法(带权图)。
-
-
编程实现:
-
0-1背包问题(动态规划)。
-
最长公共子序列问题(动态规划)。
-
-
思考题:
-
当你面对一个复杂的问题时,如何判断它可能适合用分治、动态规划还是贪心算法来解决?它们各自的适用条件和典型特征是什么?
-
在数据规模非常大(例如几十亿甚至更多)时,你会如何优化算法以适应内存或分布式环境的限制?你会用到哪些数据结构和算法思想?
-
同学们,算法是计算机科学的精髓,它不仅能让你在面试中脱颖而出,更能让你在实际工程中成为一名真正有竞争力的开发者。掌握算法,你就能以更智慧的方式解决问题,创造更高效的系统。
至此,我们已经完成了第三阶段**“全栈应用开发实战”的第三课“算法设计与分析”的所有内容。接下来,我们将正式进入Web前端开发**的世界,学习构建用户界面的技术。请大家稍作休息,我们稍后继续。
好的,同学们,我们继续第三阶段**“全栈应用开发实战”**的学习!前面我们已经打下了坚实的编程基础,学习了Python语言和各种数据结构、算法。现在,我们将把目光投向大家最熟悉也最直观的领域——Web前端开发。
Web前端,顾名思义,就是用户在浏览器中直接看到和交互的部分。它是软件的“脸面”,直接决定了用户体验。想象一下,你打开一个网站或一个手机应用,你所看到的所有界面元素、你所进行的所有点击、滑动、输入等交互,都是前端的功劳。
本节课,我们将深入学习Web前端的三大核心技术:HTML、CSS和JavaScript。它们就像是网页的“骨架”、“皮肤”和“大脑”,缺一不可。
课程3.3:Web前端基础 - HTML/CSS/JavaScript(超详细版)
一、Web前端的基本概念:网页的“骨架、皮肤与大脑”
1.1 什么是Web前端
Web前端是指构建和呈现用户界面,并允许用户与网站或Web应用程序进行交互的技术集合。它运行在用户的客户端浏览器上,直接决定了用户看到什么、如何操作以及页面的展现效果和交互能力。
-
前端的三大核心支柱:
-
HTML (HyperText Markup Language):结构(Structure)。它定义了网页的内容和语义,告诉浏览器页面上有什么(如标题、段落、图片、链接等)。
- 比喻:网页的“骨架”或“内容”。
-
CSS (Cascading Style Sheets):表现(Presentation)。它定义了网页的样式和布局,告诉浏览器内容应该长什么样(如颜色、字体、大小、位置等)。
- 比喻:网页的“皮肤”或“外观”。
-
JavaScript (JS):行为(Behavior)。它实现了网页的交互和动态效果,让网页从静态信息展示变为动态应用(如点击按钮弹出提示、表单验证、数据加载等)。
- 比喻:网页的“大脑”或“交互能力”。
-
-
前端与后端的关系:
-
前端:专注于用户界面和用户体验,负责页面显示、用户交互和部分业务逻辑的“表现层”。它运行在用户设备(浏览器、手机)上。
-
后端:专注于数据存储、复杂的业务逻辑、API接口、安全控制等“数据层”和“逻辑层”。它运行在服务器上。
-
前后端分离:现代Web开发的主流模式。前端通过HTTP(S)请求后端API来获取和提交数据,实现了前后端的独立开发和部署。
-
全栈开发者:需要掌握前端、后端、数据库、部署等整个技术栈。
-
1.2 浏览器的工作原理简述:从URL到页面的旅程
当你输入一个URL并按下回车键,浏览器是如何将它变成一个你看到的网页的呢?这是一个复杂但有趣的过程。
-
输入URL:你在浏览器地址栏输入
www.example.com。 -
DNS解析:浏览器首先会将域名(
www.example.com)发送给DNS服务器,将其解析为对应的IP地址(例如192.0.2.1)。 -
建立TCP连接:浏览器根据获取到的IP地址,与目标Web服务器建立TCP连接(通过三次握手)。
-
发送HTTP请求:建立连接后,浏览器向服务器发送HTTP请求(GET请求,请求网页内容)。
-
服务器响应:Web服务器接收请求,处理后将HTML、CSS、JavaScript、图片等资源文件通过HTTP响应发送回浏览器。
-
浏览器解析与渲染:
-
HTML解析:浏览器开始解析HTML文件,构建DOM树(Document Object Model)。DOM树是HTML文档的内存表示,它是一个树形结构,每个HTML标签都是树上的一个节点。
-
CSS解析:解析CSS文件,构建CSSOM树(CSS Object Model)。它存储了所有元素的样式信息。
-
构建渲染树(Render Tree):DOM树和CSSOM树合并,生成渲染树。渲染树只包含需要显示在页面上的元素及其计算后的样式。
-
布局(Layout/Reflow):浏览器根据渲染树计算每个元素在屏幕上的确切位置和大小。
-
绘制(Paint):浏览器将计算好的布局和样式绘制到屏幕上,呈现出最终的网页。
-
-
JavaScript执行:
-
当浏览器解析HTML遇到
<script>标签时,会暂停HTML解析,下载并执行JavaScript代码。 -
JavaScript可以动态地操作DOM树和CSSOM树,例如修改元素内容、添加/删除元素、改变样式等。这些操作会导致页面重新布局(Reflow)或重新绘制(Repaint),从而实现页面的动态效果和交互。
-
-
资源加载与并发:
- 在解析HTML时,如果遇到外部资源(如图片、CSS文件、JS文件),浏览器会并发下载这些资源。
这个过程是Web前端技术栈得以运行的基础。理解它,能帮助我们更好地优化页面加载速度和用户体验。
二、HTML5语义化与结构:网页的“骨架”与“意义”
HTML(HyperText Markup Language)是网页的骨架。HTML5是HTML的最新主要版本,它引入了许多新特性,特别是语义化标签,使得网页结构更加清晰、有意义。
2.1 HTML基本结构:网页的“模板”
一个标准的HTML5文档通常包含以下基本结构:
<!DOCTYPE html> <!-- 声明文档类型为HTML5,这是必须的 -->
<html lang="zh"> <!-- 根元素,lang="zh" 表示网页主要语言是中文,有利于搜索引擎和辅助工具 -->
<head> <!-- 头部:包含页面的元数据(不直接显示在页面上) -->
<meta charset="UTF-8"> <!-- 字符编码声明,避免乱码,推荐UTF-8 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- 视口设置,用于响应式设计 -->
<title>我的第一个网页</title> <!-- 页面标题,显示在浏览器标签页或收藏夹中 -->
<link rel="stylesheet" href="style.css"> <!-- 引入外部CSS样式表 -->
<script src="script.js"></script> <!-- 引入外部JavaScript文件 -->
</head>
<body> <!-- 主体:包含所有显示在网页上的内容 -->
<h1>欢迎来到我的网站</h1> <!-- 一级标题 -->
<p>这是一个简单的HTML页面。</p> <!-- 段落 -->
<!-- 更多内容将在这里添加 -->
</body>
</html>
2.2 常用语义化标签:让网页更有“意义”
HTML5引入了许多新的语义化标签,它们不仅定义了内容,更重要的是,它们为内容赋予了语义(Meaning),告诉浏览器和开发者这块内容是做什么用的。这对于搜索引擎优化(SEO)、无障碍访问(Accessibility)以及代码可读性都非常重要。
-
结构标签(用于划分页面区域):
-
<header>:页面的页眉,通常包含网站Logo、主标题、导航等。 -
<nav>:导航链接区域,包含网站主要导航链接。 -
<main>:页面的主要内容区域,一个页面只应该有一个<main>标签。 -
<section>:页面中的一个独立内容区块,通常包含一个标题。 -
<article>:独立的、完整的、可独立分发或重用的内容(如博客文章、新闻报道、用户评论)。 -
<aside>:与页面内容相关但又可以独立存在的内容(如侧边栏、广告、相关链接)。 -
<footer>:页面的页脚,通常包含版权信息、联系方式、次要导航等。 -
比喻:就像你写一篇报告,
<header>是封面,<nav>是目录,<main>是正文,<section>是正文的每个大章节,<article>是具体的文章,<aside>是附录,<footer>是封底。
-
-
内容标签(用于组织具体内容):
-
<h1>到<h6>:标题标签,<h1>是最高级别的标题。 -
<p>:段落标签。 -
<ul>(Unordered List):无序列表(项目符号)。 -
<ol>(Ordered List):有序列表(数字或字母编号)。 -
<li>(List Item):列表项,必须放在<ul>或<ol>内部。 -
<a>(Anchor):超链接,用于跳转到其他页面或页面内锚点。-
href属性:链接的目标URL。 -
target="_blank":在新标签页打开链接。
<a href="https://www.google.com" target="_blank">访问Google</a> -
-
<img>(Image):图片,自闭合标签。-
src属性:图片文件的URL。 -
alt属性:图片无法显示时的替代文本,对SEO和无障碍访问很重要。
<img src="logo.png" alt="公司Logo"> -
-
<span>:行内通用容器,无语义,常用于为文本片段应用样式。 -
<div>:块级通用容器,无语义,常用于页面布局。- 老师提示:避免滥用
<div>,尽量使用语义化标签。
- 老师提示:避免滥用
-
<strong>:表示文本的强调(重要性),语义上比<b>更强。 -
<em>:表示文本的着重(语调),语义上比<i>更强。
-
-
多媒体标签(HTML5新特性):
-
<audio controls>:嵌入音频播放器。 -
<video controls>:嵌入视频播放器。-
controls属性:显示默认播放器控件。 -
src属性:媒体文件URL。 -
<source>标签:支持多种格式的媒体文件。
<video controls width="300"> <source src="movie.mp4" type="video/mp4"> <source src="movie.webm" type="video/webm"> 您的浏览器不支持视频标签。 </video> -
-
<canvas>:用于在网页上绘制图形、动画等,需要JavaScript配合。
-
-
表单标签(用于用户输入数据):
-
<form>:表单容器,用于收集用户输入。-
action属性:表单提交到哪个URL。 -
method属性:提交方式(get或post)。
-
-
<input>:最常用的表单输入控件,自闭合标签。type属性:定义输入类型,如text(单行文本)、password(密码)、email(邮箱)、number(数字)、radio(单选按钮)、checkbox(复选框)、file(文件上传)、submit(提交按钮)、button(普通按钮)等。HTML5新增了许多语义化type。
<input type="text" id="username" name="username" placeholder="请输入用户名" required> <input type="email" id="email" name="email"> <input type="password" id="password" name="password"> <input type="checkbox" id="agree" name="agree" value="true" checked> <input type="radio" id="gender_m" name="gender" value="male"> -
<textarea>:多行文本输入框。 -
<select>:下拉选择框,配合<option>标签。 -
<button>:按钮。 -
<label>:为表单控件定义标签,提高可点击区域和无障碍访问。 -
<fieldset>和<legend>:将相关表单元素分组。
-
2.3 HTML属性与全局属性:标签的“附加信息”
-
属性(Attributes):
-
大多数HTML标签都可以有属性,用于提供额外的信息或配置标签的行为。属性通常以
name="value"的形式出现在标签的开始标签中。 -
常用属性:
-
id:唯一标识符,用于CSS选择器、JavaScript操作或作为页面内锚点。 -
class:类名,用于为多个元素应用相同的CSS样式或JavaScript行为。 -
style:行内样式,直接在HTML中应用CSS样式(不推荐多用)。 -
src:源文件路径(用于<img>、<script>、<video>、<audio>)。 -
href:超链接目标(用于<a>、<link>)。 -
alt:替代文本(用于<img>)。 -
title:提示信息,鼠标悬停时显示。 -
placeholder:输入框的占位文本。 -
value:表单控件的默认值。 -
disabled:禁用表单控件。 -
readonly:只读表单控件。 -
checked:复选框/单选按钮默认选中。 -
selected:下拉选择框默认选中。 -
rel:链接关系(用于<link>)。
-
-
-
全局属性(Global Attributes):
-
可以应用于所有HTML标签的属性。
-
data-*:自定义数据属性。允许你在HTML元素上存储自定义数据,可以通过JavaScript访问。非常灵活。<div id="userCard" data-user-id="12345" data-user-role="admin"> 用户信息 </div> <!-- JavaScript: const userCard = document.getElementById('userCard'); console.log(userCard.dataset.userId); // "12345" --> -
tabindex:定义元素的Tab键顺序,影响键盘导航。 -
contenteditable:使元素内容可编辑。
-
2.4 表单处理与验证:用户与网站的“桥梁”
表单是Web应用中收集用户输入的核心机制。
-
基本表单结构:
<form action="/submit-form" method="post"> <label for="name">姓名:</label> <input type="text" id="name" name="userName" required><br><br> <label for="email">邮箱:</label> <input type="email" id="email" name="userEmail" required pattern="[^@\s]+@[^@\s]+\.[^@\s]+"><br><br> <label for="message">留言:</label> <textarea id="message" name="userMessage" rows="4" cols="50"></textarea><br><br> <button type="submit">提交</button> <button type="reset">重置</button> </form>name属性:在表单提交时,服务器端通过name属性来获取对应输入字段的值。
-
表单验证(Form Validation):
-
HTML5内置校验:
-
required:字段必填。 -
type="email"/type="url"/type="number"等:浏览器会自动检查输入格式。 -
pattern="[regex]":使用正则表达式进行复杂格式校验。 -
min/max/minlength/maxlength:数值或长度范围。 -
autocomplete:是否启用自动填充。
-
-
JavaScript自定义验证:
-
对于更复杂的、动态的验证逻辑,通常需要通过JavaScript在客户端进行实时校验,并提供更友好的错误提示。
-
例如:确认密码是否一致、用户名是否已被注册(需要调用后端API)。
-
-
2.5 多媒体与嵌入:丰富网页内容
-
图片:
<img>标签,支持JPG, PNG, GIF, SVG, WebP等格式。 -
音频/视频:
<audio>和<video>标签,支持MP3, WAV, MP4, WebM等格式。-
提供
controls属性显示播放控件。 -
支持
autoplay(自动播放,通常需要静音才能自动播放)、loop(循环)、poster(视频封面图)。
-
-
<iframe>(Inline Frame):-
作用:在当前网页中嵌入另一个网页、地图、视频等内容。
-
src属性:嵌入内容的URL。 -
安全注意:
<iframe>可能存在安全风险(如点击劫持),需谨慎使用,并设置sandbox、allow等属性进行安全控制。
-
同学们,HTML是所有Web页面的基石。理解其结构、语义化标签以及各种属性,是构建高质量网页的第一步。一个好的HTML结构,不仅能让页面内容清晰,也为后续的样式和交互打下了坚实的基础。
好的,同学们,我们继续Web前端基础的学习!上一节我们详细探讨了HTML,了解了网页的“骨架”和内容语义。现在,我们要把目光转向网页的“皮肤”和“服装”——CSS(Cascading Style Sheets)。
CSS决定了网页的视觉表现和布局,它控制着字体大小、颜色、背景、边距、定位等所有我们能看到和感受到的样式。没有CSS,网页将是枯燥的纯文本黑白页面。掌握CSS,你就能将你的网页设计得美观、响应式,并充满活力。
三、CSS3高级特性与页面布局:网页的“皮肤”与“服装设计师”
3.1 CSS基础语法:样式的“规则”
CSS(层叠样式表)是一套用于描述文档样式(如HTML)的样式表语言。
-
基本语法:
选择器 { 属性: 值; 属性-1: 值1; }-
选择器(Selector):用于指定要应用样式的HTML元素。
-
属性(Property):要设置的样式特征(如
color、font-size)。 -
值(Value):属性的具体设定(如
red、16px)。
-
-
引入CSS的方式:
-
外部样式表(External Stylesheet):
-
方法:将CSS代码写入一个独立的
.css文件,然后在HTML文件的<head>标签中使用<link>标签引入。 -
优点:推荐方式,实现了HTML结构与CSS样式的彻底分离,便于管理和维护,可复用,浏览器会缓存CSS文件,加载更快。
<link rel="stylesheet" href="style.css"> -
-
内部样式表(Internal Stylesheet):
-
方法:将CSS代码写入HTML文件
<head>标签内的<style>标签中。 -
优点:适用于单个HTML页面。
-
缺点:不适合多页面共享样式。
<style> h1 { color: blue; } </style> -
-
行内样式(Inline Style):
-
方法:直接将CSS样式作为HTML标签的
style属性值。 -
优点:方便快速测试或特殊场景。
-
缺点:不推荐使用!混合了HTML结构和CSS样式,难以维护,无法复用,优先级过高(难以覆盖)。
<div style="color: red; font-size: 16px;">这是行内样式</div> -
-
-
CSS层叠性(Cascading)与优先级:
-
层叠性:多个样式规则可能同时作用于同一个元素,CSS会根据特定的规则(优先级、来源、顺序)进行层叠,决定最终哪个样式生效。
-
优先级(Specificity):
-
行内样式 (
<div style="...">):最高优先级。 -
ID选择器 (
#id):次之。 -
类选择器 (
.class)、属性选择器 ([type="text"])、伪类 (:hover):再次之。 -
标签选择器 (
div)、伪元素 (::before):再次之。 -
通配符选择器 (
*)、继承的样式:最低优先级。 -
!important:可以覆盖几乎所有优先级,但不推荐使用,因为它会破坏层叠性,导致样式难以管理。
-
-
3.2 选择器详解:精确选择你要“打扮”的元素
选择器是CSS的核心,它精确地指明了哪些HTML元素应该应用你定义的样式。
-
标签选择器(Type Selector):
-
语法:
tagname -
作用:选择所有指定标签名的元素。
-
示例:
div { ... }、p { ... }
-
-
类选择器(Class Selector):
-
语法:
.classname -
作用:选择所有
class属性包含指定类名的元素。 -
示例:
.highlight { color: yellow; }
-
-
ID选择器(ID Selector):
-
语法:
#id -
作用:选择
id属性为指定值的唯一元素。 -
示例:
#main-header { font-size: 24px; }
-
-
通用选择器(Universal Selector):
-
语法:
* -
作用:选择所有元素。
-
示例:
* { margin: 0; padding: 0; }(常用于重置默认样式)
-
-
组合选择器(Combinator Selectors):
-
后代选择器:
selector1 selector2(空格)-
选择
selector1的所有后代selector2。 -
示例:
ul li { list-style: none; }(选择所有ul内的li)
-
-
子元素选择器:
selector1 > selector2-
选择
selector1的直接子元素selector2。 -
示例:
div > p { color: blue; }(选择div的直接子元素p)
-
-
相邻兄弟选择器:
selector1 + selector2-
选择紧接在
selector1后面的第一个兄弟元素selector2。 -
示例:
h1 + p { margin-top: 10px; }
-
-
通用兄弟选择器:
selector1 ~ selector2- 选择
selector1后面的所有兄弟元素selector2。
- 选择
-
-
属性选择器(Attribute Selectors):
-
语法:
[attribute]、[attribute="value"]、[attribute~="value"](包含单词)、[attribute^="value"](以...开头)、[attribute$="value"](以...结尾)、[attribute*="value"](包含...) -
示例:
input[type="text"] { border: 1px solid gray; }
-
-
伪类(Pseudo-classes):
-
语法:
selector:pseudo-class -
作用:选择处于特定状态的元素。
-
常见:
-
:hover:鼠标悬停时。 -
:active:元素被激活时(如鼠标点击瞬间)。 -
:focus:元素获得焦点时(如输入框)。 -
:visited:链接被访问过。 -
:first-child/:last-child/:nth-child(n):作为其父元素的第一个/最后一个/第N个子元素。 -
::before/::after:伪元素,在元素内容之前/之后插入生成的内容。
-
-
示例:
a:hover { color: orange; } /* 鼠标悬停链接变橙色 */ input:focus { border-color: blue; } /* 输入框获得焦点时边框变蓝 */ p::first-line { font-weight: bold; } /* 段落第一行加粗 */
-
3.3 盒模型与布局基础:元素在页面上的“形状”
所有HTML元素在浏览器中都被渲染为一个矩形的“盒子”。理解盒模型是进行页面布局和样式调整的基础。
-
盒模型(Box Model):
-
一个元素由四个部分组成,从内到外分别是:
-
Content(内容区):元素的实际内容,如文本、图片。
-
Padding(内边距):内容区与边框之间的空间。
-
Border(边框):内容和内边距的边界。
-
Margin(外边距):元素边框以外,与其他元素之间的空间。
-
-
比喻:一个相框。Content是照片本身,Padding是相框的内衬,Border是相框的边框,Margin是相框与墙上其他画作之间的距离。
-
-
box-sizing属性:-
content-box(默认值):元素的width和height只包括内容区。Padding和Border会额外增加元素总尺寸。 -
border-box:元素的width和height包括内容区、内边距和边框。Padding和Border会被包含在指定的width和height内,不会额外增加总尺寸。这使得布局计算更加直观。 -
老师提示:在现代CSS布局中,通常会使用
* { box-sizing: border-box; }来重置所有元素的盒模型,这能大大简化布局计算。
-
-
块级元素(Block-level Elements)vs 行内元素(Inline Elements):
-
块级元素:
-
独占一行,即使内容很短也会占据整个宽度。
-
可以设置
width、height、margin、padding。 -
常见:
<div>,<p>,<h1>,<ul>,<li>。
-
-
行内元素:
-
内容有多宽就占多宽,可以与其他行内元素在同一行显示。
-
不能设置
width、height。margin和padding的垂直方向(top/bottom)设置无效。 -
常见:
<span>,<a>,<img>,<strong>。
-
-
行内块级元素(Inline-block Elements):
-
特点:既有行内元素的特性(可以并排显示),又有块级元素的特性(可以设置
width、height、margin、padding)。 -
设置:通过
display: inline-block;。
-
-
3.4 CSS布局方式:页面的“排版艺术”
CSS提供了多种布局方式,用于控制元素在页面上的排列和定位。
3.4.1 浮动布局(Float Layout)
-
原理:通过
float属性(left或right)使元素脱离文档流,浮动到父容器的左侧或右侧,其他内容会环绕它。 -
用途:早期常用于实现多列布局或图文环绕效果。
-
缺点:
-
会导致父元素高度塌陷(因为浮动元素脱离了文档流)。
-
需要使用
clear属性或BFC(Block Formatting Context)来清除浮动,避免布局混乱。
-
-
老师提示:在现代布局中,浮动布局已被Flexbox和Grid取代,不再推荐用于复杂的整体布局,但偶尔仍用于图文混排。
3.4.2 定位布局(Positioning Layout)
-
原理:通过
position属性,可以精确控制元素相对于其正常位置、父元素或浏览器视口的定位。 -
position属性值:-
static(默认值):元素在文档流中正常定位,top/right/bottom/left属性无效。 -
relative(相对定位):-
元素相对于其正常位置进行偏移。
-
不脱离文档流,会保留其在文档流中的空间。
-
-
absolute(绝对定位):-
元素相对于其最近的非
static定位的父元素进行定位。如果没有这样的父元素,则相对于<body>元素(即浏览器视口)。 -
脱离文档流,不占据空间,其他元素会当作它不存在。
-
-
fixed(固定定位):-
元素相对于**浏览器视口(Viewport)**固定定位,不随页面滚动而移动。
-
脱离文档流。常用于固定在页面顶部的导航栏或底部工具栏。
-
-
sticky(粘性定位,CSS3新增):-
结合了
relative和fixed的特点。元素在滚动到特定位置之前是相对定位,达到特定位置后变为固定定位。 -
不脱离文档流。常用于吸顶导航栏或侧边栏。
-
-
-
top,right,bottom,left属性:配合position属性使用,控制元素的偏移量。 -
z-index属性:当多个定位元素重叠时,z-index控制它们的堆叠顺序,值越大越靠上。
3.4.3 Flexbox弹性布局(Flexible Box Layout Module)
-
原理:为盒状模型提供了一种更高效、更灵活的方式来控制元素在容器内的排列、对齐和分配空间,尤其适用于一维布局(行或列)。
-
核心思想:通过设置**父容器(Flex Container)和子元素(Flex Item)**的属性来实现布局。
-
父容器属性:
-
display: flex;:将容器设置为弹性盒模型。 -
flex-direction: row | column | row-reverse | column-reverse;:主轴方向。 -
flex-wrap: nowrap | wrap | wrap-reverse;:是否换行。 -
justify-content:主轴上的对齐方式(如flex-start,center,space-between,space-around)。 -
align-items:交叉轴上的对齐方式(如flex-start,center,stretch,baseline)。 -
align-content:多行交叉轴上的对齐方式。
-
-
子元素属性:
-
flex-grow:定义子元素的放大比例。 -
flex-shrink:定义子元素的缩小比例。 -
flex-basis:定义子元素在主轴上的初始尺寸。 -
flex:flex-grow,flex-shrink,flex-basis的简写。 -
order:定义子元素的排列顺序。 -
align-self:单个子元素在交叉轴上的对齐方式。
-
-
优点:非常适合组件内部布局、导航栏、网格系统中的行或列等一维排列。
-
比喻:你有一排小盒子,Flexbox就是你的“魔术带”,可以随意调整盒子们的方向、间距和大小比例。
示例:水平垂直居中
.container {
display: flex;
justify-content: center; /* 主轴居中 */
align-items: center; /* 交叉轴居中 */
height: 200px;
border: 1px solid gray;
}
.box {
width: 50px;
height: 50px;
background-color: lightblue;
}
<div class="container">
<div class="box"></div>
</div>
3.4.4 Grid网格布局(CSS Grid Layout)
-
原理:一个强大的二维布局系统,能够同时控制行和列的布局。
-
核心思想:将页面划分为行和列的网格,然后将元素放置在这些网格单元中。
-
父容器属性:
-
display: grid;:将容器设置为网格模型。 -
grid-template-rows/grid-template-columns:定义网格的行和列的尺寸(如100px 1fr 200px)。 -
grid-gap/gap:定义网格行和列之间的间距。 -
grid-template-areas:通过命名区域来放置元素,更直观。
-
-
子元素放置:
-
grid-row-start / grid-row-end:定义元素占据的行。 -
grid-column-start / grid-column-end:定义元素占据的列。 -
grid-area:简写,用于命名区域。
-
-
优点:非常适合整个页面布局、复杂的网格系统。
-
比喻:你有一张白纸,Grid就是你的“画格纸”,可以随意绘制水平和垂直的线条,然后把内容放进画好的格子里。
示例:简单的2x2网格布局
.grid-container {
display: grid;
grid-template-columns: 1fr 1fr; /* 两列,平分空间 */
grid-template-rows: 100px 100px; /* 两行,每行100px高 */
gap: 10px; /* 格子间距 */
width: 300px;
border: 1px solid black;
}
.grid-item {
background-color: lightcoral;
padding: 10px;
text-align: center;
}
/* 放置特定元素到指定格子 */
.item1 {
grid-column: 1 / 3; /* 占据第1列到第3列,即第一行两列合并 */
}
<div class="grid-container">
<div class="grid-item item1">头部 (占据两列)</div>
<div class="grid-item">内容A</div>
<div class="grid-item">内容B</div>
</div>
3.4.5 响应式设计(Responsive Web Design):适应不同屏幕尺寸
-
概念:一种Web设计方法,使网站在不同设备(桌面、平板、手机)上都能提供最佳的浏览体验,无需为每个设备创建独立的版本。
-
核心技术:
-
媒体查询(Media Queries):
-
语法:
@media screen and (max-width: 768px) { ... } -
作用:根据设备的特性(如屏幕宽度、高度、方向、分辨率等)应用不同的CSS样式。
-
示例:
/* 默认样式,适用于大屏幕 */ .container { width: 960px; margin: 0 auto; display: flex; } /* 屏幕宽度小于768px时应用以下样式(针对平板和手机) */ @media (max-width: 768px) { .container { width: 100%; flex-direction: column; /* 在小屏幕上改为垂直堆叠 */ } }
-
-
百分比/弹性单位:
-
%:相对于父元素。 -
em:相对于当前元素的字体大小(父元素字体大小)。 -
rem:相对于根元素(html)的字体大小。 -
vw(viewport width):相对于视口宽度的1%。 -
vh(viewport height):相对于视口高度的1%。 -
min()/max()/clamp():CSS函数,用于在不同大小之间灵活选择。
-
-
图片自适应:
max-width: 100%; height: auto; -
移动优先设计(Mobile First):
- 一种设计理念,首先为移动设备设计和开发(小屏幕、资源受限),然后逐步扩展到大屏幕。这有助于确保核心功能在移动设备上得到良好体验。
-
3.5 CSS动画与过渡:让页面“动”起来
-
过渡(Transitions):
-
作用:在元素属性值改变时,实现属性值平滑的动画效果,而不是突然变化。
-
属性:
transition-property,transition-duration,transition-timing-function,transition-delay。 -
简写:
transition: property duration timing-function delay; -
示例:
.button { background-color: blue; width: 100px; transition: background-color 0.3s ease-in-out, width 0.5s; /* 多个属性过渡 */ } .button:hover { background-color: orange; width: 120px; }
-
-
动画(Animations):
-
作用:通过
@keyframes规则定义动画序列,然后通过animation属性将其应用到元素上,实现更复杂的、多阶段的动画。 -
@keyframes:定义动画的关键帧(如0%,50%,100%),在不同时间点定义不同的样式。 -
animation属性:控制动画的名称、持续时间、循环次数、方向等。 -
示例:一个简单的旋转动画
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .spinner { width: 50px; height: 50px; background-color: #3498db; animation: spin 2s linear infinite; /* 动画名称 持续时间 速度曲线 循环次数 */ }<div class="spinner"></div>
-
同学们,CSS是赋予网页生命和美感的关键。掌握选择器、盒模型、各种布局方式(特别是Flexbox和Grid)、响应式设计,以及简单的动画和过渡,你就能将你的Web页面从简单的信息展示,提升为具有视觉吸引力和良好用户体验的作品。