Lecture 1.1 - Arrays
一、数组基础:为什么需要数组?
核心问题引入
假设要读取 100 个数字并计算平均值,还要找出大于平均值的数字个数。如果为每个数字都创建一个变量,显然不现实。这时,数组就能派上用场,它可以用一个变量存储多个同类型的数据。
数组的定义与特点
- 定义:数组是相同数据类型元素的集合,有固定的长度,通过索引(从 0 开始)访问元素。
- 内存存储:数组在内存的堆区分配空间,变量存储的是数组的引用(内存地址)。
二、数组的声明与创建
1. 声明数组变量
语法:
CodeBlock Loading...
2. 创建数组(分配内存)
语法:
CodeBlock Loading...示例:
CodeBlock Loading...
3. 声明与创建合并
三、数组初始化
1. 默认值
- 数值型(如
int、double):默认值为0。 - 布尔型:默认值为
false。 - 引用类型(如
String):默认值为null。
2. 手动赋值
通过索引赋值:
CodeBlock Loading...
3. 快捷初始化(数组初始化器)
语法:
CodeBlock Loading...示例:
CodeBlock Loading...
四、数组常用操作
1. 遍历数组
普通 for 循环(带索引):
CodeBlock Loading...增强 for 循环(for-each,无索引):
CodeBlock Loading...
2. 常见算法
求和:
CodeBlock Loading...找最大值:
CodeBlock Loading...数组复制:
错误做法(引用复制):
CodeBlock Loading...正确做法(元素复制):
CodeBlock Loading...
五、数组作为方法参数
1. 传递数组(按值传递引用)
- Java 中传递数组时,实际传递的是数组的引用(内存地址),因此方法内对数组元素的修改会影响原始数组。
示例:
CodeBlock Loading...
2. 返回数组
方法可以返回一个数组,例如反转数组:
CodeBlock Loading...
六、搜索与排序算法
1. 线性搜索(适用于未排序数组)
- 原理:从数组第一个元素开始,逐个与目标值比较,直到找到或遍历完数组。
代码:
CodeBlock Loading...
2. 二分搜索(适用于已排序数组,效率更高)
- 原理:每次将数组分成两半,根据中间元素与目标值的大小关系,缩小搜索范围。
- 时间复杂度:O(log n)(最坏情况)。
代码:
CodeBlock Loading...
3. 选择排序(简单直观,效率较低)
- 原理:每次从剩余元素中找到最小值,与当前位置元素交换。
代码:
CodeBlock Loading...
七、练习与 Quiz
练习 1:数组初始化判断
下面哪个是正确的数组初始化方式?
答案:B(A 缺少长度;C 是 C/C++ 语法,Java 不支持)。
练习 2:增强 for 循环
答案:3(增强 for 循环中无法通过 num 修改原数组元素)。
练习 3:二分搜索时间复杂度
二分搜索的最坏时间复杂度是?
答案:C(每次折半,时间复杂度为对数级)。
八、总结
- 重点知识:数组的声明与创建、初始化、遍历、作为方法参数传递、搜索与排序算法。
- 常见误区:
- 数组长度不可变,创建后无法修改。
- 直接赋值
arr2 = arr1是引用复制,而非元素复制。 - 二分搜索必须在已排序数组上使用。
Lecture 1.2 - Objects and Classes
一、核心概念:对象与类
1. 什么是对象?
- 定义:对象是现实世界中可唯一标识的实体,例如“一个圆形”“一个学生”。
- 特征:
- 状态(State):用数据字段(属性)表示,如圆形的半径
radius。 - 行为(Behavior):用方法(函数)表示,如计算面积的
getArea()。
- 状态(State):用数据字段(属性)表示,如圆形的半径
2. 什么是类?
- 定义:类是对象的模板/蓝图,用于创建具有相同属性和方法的对象。
- 组成:
- 数据字段(Fields):如
private double radius;(非静态,属于每个对象)。 - 方法(Methods):如
public double getArea()(非静态方法需通过对象调用)。 - 构造函数(Constructors):用于创建对象,名称必须与类名相同,无返回类型。
- 数据字段(Fields):如
示例:Circle类
二、创建对象与内存分析
1. 对象创建步骤
- 声明引用变量:
Circle myCircle;(此时变量值为null,未指向任何对象)。 - 实例化对象:
myCircle = new Circle(5.0);(通过new关键字调用构造函数,分配内存)。
2. 内存示意图
3. 访问对象成员
- 访问属性:
对象名.属性(需注意权限,如private属性不可直接访问)。 - 调用方法:
对象名.方法名()。
示例:
三、关键知识点解析
1. 静态(Static) vs 非静态(Non-static)
| 对比项 | 静态成员(类成员) | 非静态成员(实例成员) |
|---|---|---|
| 归属 | 属于类本身,所有对象共享 | 属于单个对象 |
| 访问方式 | 直接通过类名访问(如ClassName.x) | 必须通过对象访问(如obj.x) |
| 常见用途 | 工具方法(如Math.pow())、全局计数器 | 对象的状态数据(如每个Circle的radius) |
示例:静态变量numberOfObjects
2. 访问修饰符(Visibility Modifiers)
| 修饰符 | 作用范围 | UML符号 |
|---|---|---|
public | 任何类均可访问 | + |
private | 仅当前类内部可访问 | - |
| 默认 | 同一包(package)内可访问 | 无符号 |
封装最佳实践:
将数据字段设为
private,通过访问器(getter)和修改器(setter)方法间接操作。CodeBlock Loading...
3. 基本类型 vs 引用类型
| 类型 | 示例 | 赋值行为 | 内存存储 |
|---|---|---|---|
| 基本类型 | int i = 5; | 复制值(如j = i后,j独立存储5) | 栈内存 |
| 引用类型 | Circle c = new Circle(); | 复制引用(如c2 = c1后,两者指向同一对象) | 引用在栈,对象在堆 |
注意:修改引用类型的属性会影响所有指向它的变量!
四、UML类图快速入门
UML类图用于可视化类的结构,常见符号:
五、实战练习
练习1:定义学生类
需求:
- 定义
Student类,包含私有字段name(String)、age(int)、isScienceMajor(boolean)。 - 提供构造函数初始化
name和age,isScienceMajor默认false。 - 提供
getter和setter方法。 - 在
main方法中创建学生对象,设置专业为true,并输出信息。
参考答案:
练习2:静态方法应用
需求:在Circle类中添加静态方法calculateArea(double radius),无需创建对象即可计算面积。
参考答案:
六、常见问题解答
为什么构造函数没有返回类型?
构造函数的作用是初始化对象,不是“返回”对象,因此无需声明返回类型(包括void)。静态变量什么时候初始化?
静态变量在类加载时初始化,早于对象创建,且仅初始化一次。this关键字的作用是什么?this指代当前对象,用于区分方法参数与类的字段(如this.radius = radius;)。
Lecture 2.1 - Thinking in Objects
第一部分:核心概念
1. 不可变对象与类(Immutable Objects and Classes)
定义:对象一旦创建,内容不可修改,其类称为不可变类。
条件(缺一不可):
- 所有数据字段为
private - 无
set方法(mutator) - 不返回可变对象的引用(如数组、自定义对象)
示例:不可变的 Circle 类
反例:可变的 Student 类
为什么可变?
练习题1:
判断以下类是否为不可变类?为什么?
2. 变量作用域(Scope of Variables)
| 类型 | 作用域 | 初始化规则 |
|---|---|---|
| 局部变量 | 从声明处到所在代码块结束 | 必须显式初始化后才能使用 |
| 实例变量 | 整个类范围 | 自动赋默认值(如 int=0) |
| 静态变量 | 整个类范围 | 自动赋默认值 |
示例:
练习题2:
指出以下代码的错误:
3. this 关键字(The this Keyword)
作用:
引用当前对象的成员:当局部变量与成员变量同名时,用
this区分。CodeBlock Loading...在构造方法中调用重载的构造方法:必须作为构造方法的第一条语句。
CodeBlock Loading...
注意:静态方法中不能使用 this(静态方法属于类,不属于对象)。
4. 类的抽象与封装(Class Abstraction and Encapsulation)
核心思想:
- 抽象:对外暴露功能(API),隐藏实现细节(如方法内部逻辑、数据存储方式)。
- 封装:通过
private修饰符隐藏数据字段,仅通过公共方法(getter/setter)访问。
示例:Loan 类的封装
优点:
- 数据安全(防止非法修改)
- 易维护(修改内部实现不影响外部调用)
第二部分:具体类设计实践
1. 设计 Loan 类
需求:计算贷款的月还款额和总还款额。
属性:年利率、贷款年限、贷款金额、创建日期
方法:计算月还款(getMonthlyPayment)、总还款(getTotalPayment)
关键代码:
练习题3:
修改 Loan 类,添加一个 getDailyPayment() 方法,计算每日还款额(假设一年 365 天)。
2. 设计 BMI 类
需求:计算身体质量指数(BMI)并判断健康状态。
公式:
状态判断:
- <16:严重偏瘦
- 16~18:偏瘦
- 18~24:正常
- 24~29:超重
- ≥29:严重超重
关键代码:
练习题4:
修改 BMI 类,增加一个构造方法,允许传入体重(公斤)和身高(米),并重载 getBMI() 方法。
第三部分:常用类详解
1. String 类(不可变性与常用方法)
不可变性:
字符串比较:
| 方法 | 用途 |
|---|---|
equals() | 比较内容是否相等(区分大小写) |
== | 比较引用是否指向同一对象 |
equalsIgnoreCase() | 忽略大小写比较内容 |
示例:
常用方法:
截取子串:
substring(start, end)(左闭右开)CodeBlock Loading...- 查找字符/子串:
indexOf("sub")(返回首次出现的索引,未找到返回 -1) - 替换:
replace("old", "new")(返回新字符串,原字符串不变)
2. StringBuilder 与 StringBuffer
区别:
StringBuilder:非线程安全,效率高(推荐单线程使用)StringBuffer:线程安全,效率低(适合多线程环境)
核心操作:
何时使用?:需要频繁修改字符串时(如循环拼接),用 StringBuilder 代替 String,避免生成大量临时对象。
3. 正则表达式(Regular Expressions)
作用:用于字符串匹配、验证、替换、分割。
示例:
- 验证邮箱:
^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$ - 提取数字:
String[] nums = "a1b2c3".split("\D"); // ["1", "2", "3"] - 替换特殊字符:
"a$b#c".replaceAll("[\$#]", "X"); // "aXbXc"
练习题5:
编写正则表达式,验证手机号(以 1开头,第二位 3-9,共11位数字)。
第四部分:综合练习
设计 Course 类
需求:管理课程的学生列表,支持添加学生、获取学生列表、动态扩容。
属性:课程名、学生数组、学生人数
关键代码:
Lecture 2.2 - Inheritance and Polymorphism
一、继承的基本概念
1. 为什么需要继承?
- 动机:当多个类(如Circle、Rectangle)有共同属性(color、filled)和方法(getArea、getPerimeter)时,通过继承可以避免代码冗余。
- 父类(超类):提取公共属性和方法,如
GeometricObject。 - 子类:继承父类,并添加特有的属性和方法,如
Circle添加radius和getDiameter。
- 父类(超类):提取公共属性和方法,如
2. 如何声明子类?
语法:使用
extends关键字。CodeBlock Loading...子类继承父类的哪些成员?
- 继承:非
private的属性和方法(如color是private,需通过getColor()访问)。 - 不继承:构造方法(需显式或隐式调用父类构造方法)。
- 继承:非
二、构造函数与super关键字
1. 构造函数链(Constructor Chaining)
规则:子类构造函数必须先调用父类构造函数(通过
super()或this()),默认调用父类无参构造函数。CodeBlock Loading...示例分析:
CodeBlock Loading...
2. super关键字的作用
- 调用父类构造函数:必须作为子类构造函数的第一行。
调用父类被重写的方法:
CodeBlock Loading...
三、方法重写(Override)与重载(Overload)
1. 方法重写(Override)
定义:子类重新实现父类的非
private、非static、非final方法,方法签名必须完全一致。CodeBlock Loading...应用场景:多态的基础(后文会讲)。
2. 方法重载(Overload)
定义:同一类中,方法名相同但参数列表不同(类型、数量、顺序),与返回值无关。
CodeBlock Loading...
3. 对比表格
| 特征 | 重写(Override) | 重载(Overload) |
|---|---|---|
| 发生范围 | 子类与父类之间 | 同一类中 |
| 方法签名 | 必须相同 | 必须不同(参数列表) |
| 访问权限 | 子类方法不能比父类更严格(如父类public,子类不能是protected) | 无限制 |
| 多态性 | 支持(动态绑定) | 不支持(静态绑定) |
练习1:判断以下代码是重写还是重载?
四、多态(Polymorphism)与动态绑定(Dynamic Binding)
1. 多态的本质
定义:父类引用可以指向子类对象,运行时根据实际对象类型调用方法。
CodeBlock Loading...优势:代码通用性强,如
displayGeometricObject方法可接收任何GeometricObject子类对象。CodeBlock Loading...
2. 动态绑定机制
- 过程:JVM在运行时根据对象的实际类型(而非引用类型)确定调用哪个方法。
- 若对象是
Circle,调用Circle的getArea();若是Rectangle,调用Rectangle的getArea()。
- 若对象是
示例:
CodeBlock Loading...
五、类型转换与instanceof操作符
1. 向上转型(Upcasting)
自动转换:子类对象 → 父类引用(安全,因为子类是父类的一种)。
CodeBlock Loading...
2. 向下转型(Downcasting)
强制转换:父类引用 → 子类对象(需确保引用实际指向子类对象,否则抛出
ClassCastException)。CodeBlock Loading...
3. instanceof操作符
作用:在向下转型前检查对象类型,避免异常。
CodeBlock Loading...
六、Object类的常用方法
1. toString()方法
- 默认实现:返回
类名@哈希码(如Circle@123456),无实际意义。 重写建议:返回对象属性的字符串表示。
CodeBlock Loading...
2. equals()方法
- 默认实现:比较对象引用地址(
this == obj),而非内容。 重写逻辑:
- 判断是否为同一对象(
this == obj)。 - 判断
obj是否为null或类型是否匹配(用instanceof)。 - 比较关键属性是否相等。
CodeBlock Loading...- 判断是否为同一对象(
七、容器类:ArrayList与自定义栈(MyStack)
1. ArrayList的优势
- 动态扩容:无需指定初始大小,自动扩展。
泛型支持:避免类型转换警告,如
ArrayList<Circle>只能存储Circle对象。CodeBlock Loading...
2. 自定义栈(MyStack)
底层实现:用
ArrayList存储元素,实现栈的push(入栈)、pop(出栈)等操作。CodeBlock Loading...
八、访问修饰符:protected与final
1. protected修饰符
- 作用:允许同一包内的类和子类(即使不同包)访问。
- 对比表格:
| 修饰符 | 同一类 | 同一包 | 子类(不同包) | 其他包 |
|---|---|---|---|---|
public | ✅ | ✅ | ✅ | ✅ |
protected | ✅ | ✅ | ✅ | ❌ |
默认 | ✅ | ✅ | ❌ | ❌ |
private | ✅ | ❌ | ❌ | ❌ |
2. final修饰符
final变量:常量,不可修改(如final static double PI = 3.14;)。final方法:不可被重写(防止子类修改核心逻辑)。final类:不可被继承(如String类)。
九、课堂测验与巩固
测验1:继承与构造函数
答案:B(子类构造函数调用super(1),执行父类A(int x)构造函数)。
测验2:重写与多态
答案:Woof(动态绑定,调用Dog的speak())。
Lecture 3 - Abstract Classes and Interfaces
一、核心知识点总结
1. 抽象类(Abstract Classes)
- 定义:用
abstract修饰的类,包含抽象方法(abstract方法,无方法体)和具体方法。- 例:
GeometricObject是抽象类,包含getArea()、getPerimeter()抽象方法。
- 例:
- 特点:
- 不能直接实例化(不能用
new创建对象),但可作为父类被继承。 - 非抽象子类必须实现父类的所有抽象方法。
- 抽象类可包含构造方法,供子类通过
super()调用。
- 不能直接实例化(不能用
- 用途:定义通用模板,强制子类实现特定方法(如几何图形的面积计算)。
2. 接口(Interfaces)
- 定义:用
interface声明,仅包含 抽象方法 和 常量(默认public static final)。- 例:
Edible接口定义howToEat()方法,Comparable接口定义compareTo()方法。
- 例:
- 特点:
- 类通过
implements实现接口,需重写所有接口方法(默认public abstract)。 - 支持多继承(一个类可实现多个接口),弥补Java单继承限制。
- 接口可继承其他接口(
interface A extends B, C)。
- 类通过
- 用途:定义行为规范(如“可比较”“可克隆”),解耦实现与接口。
3. 关键接口示例
Comparable接口:- 用于对象排序(如
Arrays.sort()),需实现compareTo(Object o)方法。 - 例:自定义
ComparableRectangle类,通过面积比较大小。
- 用于对象排序(如
Cloneable接口:- 标记接口(无方法),允许对象通过
clone()方法克隆。 - 注意:
Object.clone()是浅拷贝,深拷贝需手动重写clone()。
- 标记接口(无方法),允许对象通过
4. 抽象类 vs. 接口
| 对比维度 | 抽象类 | 接口 |
|---|---|---|
| 成员 | 可包含抽象方法、具体方法、变量 | 只能包含抽象方法和常量(public static final) |
| 继承/实现 | 单继承(extends) | 多实现(implements) |
| 构造方法 | 有(供子类调用) | 无 |
| 实例化 | 不能直接实例化 | 不能直接实例化 |
| 设计目的 | 定义类的模板或部分实现 | 定义行为规范(“能做什么”) |
5. 包装类(Wrapper Classes)
- 作用:将基本数据类型(如
int、double)封装为对象,便于集合类使用。 - 常见类:
Integer、Double、Boolean等,均继承自Number类。 特性:
- 不可变(对象创建后值不可改)。
支持自动装箱/拆箱(JDK 1.5+):
CodeBlock Loading...
常用方法:
- 转换:
parseInt(String s)、doubleValue()。 - 比较:
compareTo()(实现Comparable接口)。
- 转换:
6. 大数类(BigInteger & BigDecimal)
- 用途:处理超出基本类型范围的大整数(
BigInteger)或高精度小数(BigDecimal)。 特点:
- 不可变,方法返回新对象(如
add()、multiply())。 BigDecimal需指定精度和舍入模式:CodeBlock Loading...
- 不可变,方法返回新对象(如
7. 案例:Rational类(有理数)
- 功能:实现有理数的加减乘除、比较和类型转换(继承
Number接口)。 - 关键点:
- 重写
compareTo()实现比较逻辑。 - 使用
gcd()方法约分,确保分母为正。
- 重写
二、常见问题与解答
- Q:抽象类可以没有抽象方法吗?
A:可以。抽象类的存在意义是禁止实例化,即使没有抽象方法,也可作为基类强制子类继承。 - Q:接口能继承类吗?
A:不能。接口只能继承其他接口(interface A extends B),类通过implements实现接口。 - Q:为什么包装类是不可变的?
A:为保证线程安全和哈希值稳定(如作为HashMap键时),包装类的内部状态不可修改。 - Q:深拷贝和浅拷贝的实现区别?
A:浅拷贝直接复制引用(super.clone()),深拷贝需手动创建新对象并复制引用字段(如h.whenBuilt = (Date)whenBuilt.clone();)。
Lecture 4 - Generics
一、泛型基础:为何需要泛型?
核心作用
泛型能够将类型检查的阶段提前到编译期,避免在运行时出现类型错误,同时还能实现代码的复用。下面通过一个示例来对比说明:
无泛型(存在风险)
CodeBlock Loading...存在的问题:在运行时才能发现类型不匹配的问题。
有泛型(更安全)
CodeBlock Loading...优势体现:在编译阶段就能捕获错误,代码的可读性也更强。
关键概念
泛型的本质是对类型进行参数化。就像定义一个“模板”,在使用的时候再指定具体的类型。例如:
- 泛型类模板:
class GenericStack<E>E是类型参数(也被称为类型变量),在创建实例时,需要用具体的类型(如Integer、String)来替换它。
- 实例化方式:
GenericStack<String> stack = new GenericStack<>();
二、动手实践:定义泛型类与接口
案例:自定义泛型栈(GenericStack)
使用方式:
练习 1
尝试定义一个泛型队列GenericQueue<E>,要求包含enqueue(E element)(入队)和dequeue()(出队)方法。
三、深入探究:泛型方法与有界类型
1. 泛型方法
泛型方法可以独立于类而存在,在方法名前使用<T>来声明类型参数。
2. 有界类型参数(Bounded Type Parameters)
通过extends关键字来限制类型参数的范围,例如<E extends Comparable<E>>表示E必须实现Comparable接口。
案例:排序方法
练习 2
定义一个泛型方法max(E a, E b),用于返回两个可比较对象中的最大值,要求使用有界类型参数。
四、通配符(Wildcards):解决类型兼容性问题
通配符用于处理泛型类型之间的继承关系,常见的有以下三种:
| 通配符 | 含义 |
|---|---|
? | 无界通配符,表示任意类型 |
? extends T | 上界通配符,表示类型必须是T的子类(包括T本身) |
? super T | 下界通配符,表示类型必须是T的父类(包括T本身) |
案例:计算栈中数字的最大值
注意事项
- 父子类型关系:
GenericStack<Integer>不是GenericStack<Number>的子类,但GenericStack<Integer>是GenericStack<? extends Number>的子类。 - 写入限制:使用
? extends T时,无法向集合中写入元素(除了null),因为类型不确定;使用? super T时,允许写入T及其子类元素。
练习 3
思考以下代码是否能编译通过,并解释原因:
五、类型擦除(Type Erasure):Java 泛型的实现原理
核心机制
- 编译器会在编译阶段移除泛型类型信息,将其替换为原始类型(如
E替换为Object)。 因此,泛型类型在运行时并不存在,例如:
CodeBlock Loading...这是因为擦除后两者的类型都是
GenericStack(原始类型)。
限制条件
由于类型擦除的存在,泛型有以下限制:
- 不能实例化泛型类型:
new E()是不允许的,因为运行时E会被擦除为Object,无法确定具体类型。 - 静态成员不能使用类型参数:静态变量或方法属于类级别,而类型参数是实例级别的。
- 数组操作受限:不能创建泛型数组,如
E[] array = new E[10];,但可以使用ArrayList<E>。
六、实战应用:设计泛型矩阵类
文档中提供了GenericMatrix<E>的示例,它是一个抽象类,通过泛型实现了矩阵的加法和乘法运算,具体的元素操作由子类(如IntegerMatrix、RationalMatrix)来实现。
练习 4
尝试为GenericMatrix添加一个RationalMatrix子类,用于处理有理数矩阵的运算(有理数类Rational已在文档中定义)。
七、常见问题与最佳实践
- 原始类型(Raw Types):为了兼容旧代码,可以使用原始类型(如
ArrayList),但这会失去泛型的类型安全保障,不建议在新代码中使用。 - 优先使用泛型集合:避免使用
ArrayList,而应使用ArrayList<String>等参数化类型。 - 合理使用通配符:在需要处理类型层次结构时,使用通配符来提高代码的灵活性。
总结:知识图谱
Lecture 5 - Lists, Stacks, Queues, and Priority Queues
第一部分:Java集合框架概述
1. 集合框架的核心结构
Java集合框架(Java Collections Framework, JCF)是一组用于存储和操作数据的API,包含接口、抽象类和具体类。
- 接口:定义操作规范(如
Collection、List、Queue)。 - 抽象类:提供部分实现(如
AbstractCollection、AbstractList)。 - 具体类:实现具体数据结构(如
ArrayList、LinkedList、PriorityQueue)。
2. 核心接口分类
- 单元素集合(
Collection):存储独立元素,分为:- 有序列表(
List):允许重复,有序(如ArrayList、LinkedList)。 - 集合(
Set):不允许重复(如HashSet、TreeSet)。 - 队列(
Queue):先进先出(FIFO)或优先级排序(如PriorityQueue)。
- 有序列表(
- 键值对集合(
Map):存储键值对(如HashMap、TreeMap)。
第二部分:列表(List)
1. List接口特点
- 有序性:元素按插入顺序存储,可通过索引访问。
- 允许重复:可存储相同元素。
- 核心方法:
add(index, element):在指定位置插入元素。get(index):获取指定索引的元素。remove(index):删除指定索引的元素。size():获取元素个数。
2. 实现类:ArrayList vs. LinkedList
| 特性 | ArrayList | LinkedList |
|---|---|---|
| 数据结构 | 动态数组 | 双向链表 |
| 随机访问 | 快(O(1)) | 慢(O(n),需遍历链表) |
| 插入/删除 | 尾部快,中间/头部慢(需移动元素) | 快(O(1),仅需修改指针) |
| 适用场景 | 频繁查询、较少增删 | 频繁增删、首尾操作 |
3. 示例代码:List基本操作
第三部分:栈(Stack)
1. 栈的特点
- 后进先出(LIFO):最后插入的元素最先取出。
- 核心方法:
push(element):压入元素(栈顶)。pop():弹出栈顶元素(并删除)。peek():查看栈顶元素(不删除)。empty():判断栈是否为空。
2. 示例代码:栈的基本操作
第四部分:队列(Queue)和优先队列(PriorityQueue)
1. 队列(Queue)
- 先进先出(FIFO):元素从队尾插入,队头取出。
- 核心方法:
offer(element):添加元素到队尾(推荐用此方法,避免异常)。poll():取出队头元素(队列为空时返回null)。peek():查看队头元素(队列为空时返回null)。
2. 优先队列(PriorityQueue)
- 按优先级排序:元素按自然顺序(
Comparable)或自定义顺序(Comparator)排列,优先级高的元素先取出。 - 示例代码:优先队列排序 ```java import java.util.PriorityQueue;
public class PriorityQueueDemo {
}
java import java.util.ArrayList; import java.util.Collections; import java.util.List;
public class CollectionsDemo {
}
Java import java.util.Stack;
public class PostfixCalculator {
}
java // 空 HashSet Set<String> set1 = new HashSet<>();
// 从现有集合创建(如 List) List<String> list = Arrays.asList("Apple", "Banana"); Set<String> set2 = new HashSet<>(list);
// 指定初始容量和负载因子(默认容量16,负载因子0.75) Set<Integer> set3 = new HashSet<>(20, 0.8f);
java Set<String> cities = new HashSet<>(); cities.add("London"); cities.add("Paris"); cities.add("New York"); cities.add("New York"); // 重复,不会被添加 System.out.println(cities); // 输出:[London, Paris, New York](顺序不确定)
java for (String city : cities) {
}
java cities.forEach(city -> System.out.print(city.toLowerCase() + " "));
java Set<String> linkedSet = new LinkedHashSet<>(); linkedSet.add("A"); linkedSet.add("B"); linkedSet.add("C"); System.out.println(linkedSet); // 输出:[A, B, C](顺序与插入一致)
java Set<String> treeSet = new TreeSet<>(); treeSet.add("Z"); treeSet.add("A"); treeSet.add("B"); System.out.println(treeSet); // 输出:[A, B, Z](升序排列)
java // 按字符串长度降序排列 Set<String> treeSet = new TreeSet<>(Comparator.comparingInt(String::length).reversed()); treeSet.add("Apple"); // 5字母 treeSet.add("Banana"); // 6字母 treeSet.add("Cat"); // 3字母 System.out.println(treeSet); // 输出:[Banana, Apple, Cat]
java // HashSet 耗时约 10-50ms long time = System.currentTimeMillis(); for (int i = 0; i < 100000; i++) {
} System.out.println("HashSet耗时:" + (System.currentTimeMillis() - time) + "ms");
java // 空 HashMap Map<String, Integer> map1 = new HashMap<>();
// 初始化键值对 Map<String, Integer> map2 = new HashMap<>() {{
}};
// 从现有 Map 创建 Map<String, Integer> map3 = new HashMap<>(map2);
java map1.put("Orange", 30); // 添加 map1.put("Apple", 15); // 更新 Apple 的值为 15
java int value = map1.get("Apple"); // value = 15
java // 遍历所有键 for (String key : map1.keySet()) {
}
// 遍历所有键值对(推荐) for (Map.Entry<String, Integer> entry : map1.entrySet()) {
}
// 使用 forEach() map1.forEach((key, value) -> System.out.println(key + ": " + value));
java Map<String, Integer> linkedMap = new LinkedHashMap<>(16, 0.75f, true); linkedMap.put("A", 1); linkedMap.put("B", 2); linkedMap.get("A"); // 访问键 "A" System.out.println(linkedMap); // 输出:[B, A]("A" 被移动到最后)
java Map<String, Integer> treeMap = new TreeMap<>(); treeMap.put("Z", 26); treeMap.put("A", 1); treeMap.put("B", 2); System.out.println(treeMap); // 输出:{A=1, B=2, Z=26}(键升序)
java Map<String, Integer> treeMap = new TreeMap<>(Comparator.comparingInt(Map.Entry::getValue).reversed()); treeMap.put("Apple", 15); treeMap.put("Banana", 20); treeMap.put("Cherry", 10); System.out.println(treeMap); // 输出:{Banana=20, Apple=15, Cherry=10}(值降序)
java
- 复杂度:$O(n)$(最坏情况需遍历所有元素)。
二分搜索:
- 条件:数组必须有序。
代码:
CodeBlock Loading...复杂度:$O(\log n)$(每次将问题规模减半)。
2. 选择排序
- 思想:每次从未排序部分选择最小元素,与未排序部分的第一个元素交换。
代码:
CodeBlock Loading...复杂度:$O(n^2)$(双重循环)。
3. 插入排序
- 思想:将未排序元素逐个插入已排序部分的正确位置。
代码:
CodeBlock Loading...复杂度:$O(n^2)$(最坏情况下需移动所有元素)。
第三讲:递归与动态规划
1. 斐波那契数列的递归实现
- 递归公式:$fib(n) = fib(n-1) + fib(n-2)$。
代码:
CodeBlock Loading...复杂度:$O(2^n)$(重复计算大量子问题)。
2. 动态规划优化斐波那契数列
- 思想:用数组存储已计算的子问题结果,避免重复计算。
代码:
CodeBlock Loading...复杂度:$O(n)$(线性时间)。
第四讲:分治法与回溯法
1. 最近点对问题(分治法)
- 步骤:
- 将点按x坐标排序。
- 递归求解左右两半的最近点对。
- 合并时检查中间带内的点对。
- 复杂度:$O(n \log n)$(排序和递归合并)。
2. 八皇后问题(回溯法)
- 思想:逐行放置皇后,若当前位置冲突则回溯到上一行。
代码关键逻辑:
CodeBlock Loading...复杂度:$O(n!)$(理论上最坏情况,但通过剪枝优化后实际效率更高)。
第五讲:其他高效算法
1. 欧几里得算法(求GCD)
- 思想:利用$gcd(m, n) = gcd(n, m % n)$。
代码:
CodeBlock Loading...复杂度:$O(\log n)$。
2. 筛法求素数(Sieve of Eratosthenes)
- 思想:标记非素数,从2开始筛去所有倍数。
代码:
CodeBlock Loading...复杂度:$O(n \log \log n)$。
总结与练习
- 大O表示法:分析算法复杂度的核心工具,关注主导项。
- 排序算法:选择排序和插入排序均为$O(n^2)$,适合小规模数据。
- 递归与动态规划:动态规划通过存储子问题结果优化递归的指数复杂度。
- 分治与回溯:分治法将问题分解为独立子问题,回溯法通过剪枝避免无效搜索。
练习题:
分析以下代码的时间复杂度:
CodeBlock Loading...
答案:
这段代码的时间复杂度为$O(n^2)$($n$为数组长度)。
- 分析过程: 代码的核心是两层嵌套循环,用于打印数组中所有不重复的元素对(即每个元素对$(arr[i], arr[j])$满足$i < j$)。
1. 外层循环的迭代次数
外层循环的变量$i$从$0$遍历到$arr.length - 1$(共$n$次迭代,$n = arr.length$)。
2. 内层循环的迭代次数
对于外层循环的每一次迭代$i$,内层循环的变量$j$从$i + 1$遍历到$arr.length - 1$。因此:
- 当$i = 0$时,内层循环执行$n - 1$次($j = 1, 2, ..., n-1$);
- 当$i = 1$时,内层循环执行$n - 2$次($j = 2, 3, ..., n-1$);
- ...
- 当$i = n - 2$时,内层循环执行$1$次($j = n - 1$);
- 当$i = n - 1$时,内层循环不执行($j = n$超出数组范围)。
3. 总操作次数
总操作次数为内层循环所有迭代次数的和:
4. 时间复杂度结论
根据大$O$表示法的规则(忽略低阶项和常数系数),$\frac{n(n-1)}{2}$可简化为$O(n^2)$。 结论:该代码的时间复杂度为$O(n^2)$(平方阶)。
- 用动态规划实现斐波那契数列,尝试优化空间复杂度。
答案:
动态规划实现斐波那契数列(空间优化版)
斐波那契数列的定义为:
($n \geq 2$)
传统动态规划方法需要使用数组存储中间结果(空间复杂度$O(n)$),但通过观察可以发现,计算$F(n)$仅需前两项$F(n-1)$和$F(n-2)$,因此可以用两个变量代替数组,将空间复杂度优化至$O(1)$。
实现代码(Java)
代码说明
- 边界条件处理:
当$n = 0$或$n = 1$时,直接返回$0$或$1$,无需计算。 - 变量初始化:
prev1初始化为$F(0) = 0$(对应$F(n-2)$);prev2初始化为$F(1) = 1$(对应$F(n-1)$)。
- 迭代计算:
从$i = 2$开始循环至$n$,每次计算当前项$F(i) = prev1 + prev2$,然后更新prev1和prev2为下一次迭代的前驱值。 - 空间优化:
仅用两个变量保存前驱状态,避免了数组存储,空间复杂度从$O(n)$优化至$O(1)$。
复杂度分析
- 时间复杂度:$O(n)$(仅需一次循环遍历到$n$)。
- 空间复杂度:$O(1)$(仅用两个变量保存前驱状态)。
测试结果
运行 main 方法输出前10项斐波那契数:
Lecture 9 - Sorting
第一部分:排序算法基础
1. 什么是排序算法?
排序算法是将一组数据按特定顺序(如升序、降序)排列的方法。评价标准包括:
- 时间复杂度:算法运行所需时间(如 O(n²)、O(n log n))。
- 空间复杂度:算法占用的额外内存空间(如原地排序 O(1))。
- 稳定性:相同元素的相对顺序在排序后是否保持不变。
2. 常见排序算法分类
| 算法名称 | 时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|
| 冒泡排序 | O(n²) | O(1) | 稳定 |
| 归并排序 | O(n log n) | O(n) | 稳定 |
| 快速排序 | 平均 O(n log n) | O(log n) | 不稳定 |
| 堆排序 | O(n log n) | O(1) | 不稳定 |
第二部分:冒泡排序(Bubble Sort)
1. 核心思想
通过反复比较相邻元素,将较大的元素逐步“冒泡”到数组末尾。
- 示例:排序
[4, 3, 2, 1]- 第1轮:比较
4↔3、4↔2、4↔1,最大元素4到位,数组变为[3, 2, 1, 4]。 - 第2轮:比较
3↔2、3↔1,次大元素3到位,数组变为[2, 1, 3, 4]。 - 第3轮:比较
2↔1,数组有序[1, 2, 3, 4]。
- 第1轮:比较
2. 代码实现(Java)
3. 时间复杂度
- 最好情况(数组已排序):O(n)(仅需1轮遍历)。
- 最坏情况(完全逆序):O(n²)(需 n(n-1)/2 次比较)。
🎯 练习1
用冒泡排序对 [5, 1, 4, 2, 8] 排序,手动模拟每一轮过程。
第三部分:归并排序(Merge Sort)
1. 核心思想(分治算法)
- 分解:将数组分成两半,递归排序每一半。
- 合并:将两个已排序的子数组合并成一个有序数组。
- 示例:排序
[5, 2, 9, 1] - 分解:
[5,2]和[9,1]→ 继续分解为[5],[2],[9],[1]。 - 合并:
[2,5]和[1,9]→ 最终合并为[1, 2, 5, 9]。
- 示例:排序
2. 合并操作(双指针法)
3. 时间复杂度
- 每一层合并时间为 O(n),递归深度为 log n,总时间 O(n log n)。
- 空间复杂度 O(n)(需临时数组存储合并结果)。
🎯 练习2
归并排序的稳定性如何?为什么?
第四部分:快速排序(Quick Sort)
1. 核心思想(分治算法)
- 选枢轴:选择一个元素(如第一个元素)作为枢轴。
- 分区:将数组分为两部分,左半部分 ≤ 枢轴,右半部分 > 枢轴。
- 递归排序:对左右两部分递归应用快速排序。
- 示例:排序
[5, 2, 9, 3, 8],选5为枢轴 - 分区后:
[2, 3]5[9, 8]→ 递归排序左右部分,最终得到[2, 3, 5, 8, 9]。
- 示例:排序
2. 分区操作(双指针法)
3. 时间复杂度
- 平均情况:O(n log n)(枢轴平衡分区)。
- 最坏情况:O(n²)(枢轴每次为最小/最大值,如已排序数组)。
🎯 练习3
快速排序的空间复杂度为什么是 O(log n)?最坏情况如何优化?
第五部分:二叉堆与堆排序(Heap Sort)
1. 二叉堆(最大堆)
- 定义:完全二叉树,每个节点 ≥ 子节点(最大堆)。
- 数组表示:根节点下标0,左子节点2i+1,右子节点2i+2,父节点(i-1)/2。
- 操作:
- 上浮:插入元素后,与父节点比较并交换,直到堆性质满足。
- 下沉:删除根节点后,与子节点比较并交换,重建堆。
2. 堆排序步骤
- 建堆:将数组转换为最大堆(自底向上调整)。
- 排序:重复删除根节点(最大值),放到数组末尾,重建堆。
- 示例:数组
[10, 5, 3, 4, 1] - 建堆后:
[10, 5, 3, 4, 1](根节点最大)。 - 第1次删除10,数组变为
[5,4,3,1,10],重建堆后根节点5。 - 最终排序为
[1, 3, 4, 5, 10]。
- 示例:数组
3. 代码片段(建堆)
4. 时间复杂度
- 建堆时间 O(n),排序时间 O(n log n),总时间 O(n log n)。
第六部分:总结与对比
| 算法 | 核心思想 | 最佳场景 |
|---|---|---|
| 冒泡排序 | 相邻交换 | 小规模数据,教育场景 |
| 归并排序 | 分治+合并 | 大规模数据,需稳定性 |
| 快速排序 | 分治+分区 | 通用场景,效率最高 |
| 堆排序 | 二叉堆+删除根 | 原地排序,内存敏感场景 |
课后任务
- 编程练习:
- 实现冒泡排序,测试最好/最坏情况性能。
- 用归并排序对
[3, 1, 4, 1, 5, 9, 2, 6]排序,输出每一步合并结果。
- 思考问题:
- 为什么快速排序平均效率高于归并排序?
- 堆排序如何实现原地排序?
Lecture 10 - Graphs and Applications
第一讲:图的基本概念
1.1 什么是图?
- 定义:图是由 顶点(Vertices/Nodes) 和 边(Edges) 组成的结构,记作 。
- 顶点:表示对象(如城市、用户、节点)。
- 边:表示对象之间的关系(如路线、连接、依赖)。
- 示例:
- 社交网络:顶点是用户,边是“关注”关系(有向边)。
- 城市地图:顶点是城市,边是道路(无向边,可能带距离权重)。
1.2 图的分类
| 分类维度 | 类型 | 特点 |
|---|---|---|
| 边的方向 | 无向图 | 边无方向(如 与 是同一条边) |
| 有向图 | 边有方向(如 表示从 A 到 B 的单向关系) | |
| 边的权重 | 无权图 | 边仅表示连接,无数值属性 |
| 有权图 | 边带有权重(如距离、成本、时间) | |
| 特殊结构 | 简单图 | 无自环(顶点到自身的边)和并行边(两顶点间多条边) |
| 非简单图 | 允许自环或并行边 | |
| 连通性 | 连通图 | 任意两顶点之间存在路径 |
| 非连通图 | 存在至少两顶点无法通过路径连接 |
练习1:判断以下场景属于哪种图:
- 地铁线路图(站点连接,无方向)→ 无向无权图
- 航班路线图(城市间单向航线,带飞行时间)→ 有向有权图
- 化学分子结构(原子为顶点,化学键为边,无权重)→ 无向简单图
1.3 关键术语
- 相邻顶点(Adjacent Vertices):通过一条边直接连接的顶点(如 和 相邻)。
- 顶点的度(Degree):
- 无向图:顶点关联的边数(如顶点 有3条边,度为3)。
- 有向图:分为 入度(In-Degree) 和 出度(Out-Degree)。
- 环(Cycle):起点和终点相同的路径(如 )。
- 树(Tree):连通且无环的无向图,边数为 。
- 生成树(Spanning Tree):包含图中所有顶点的树(边数为 ,连通无环)。
第二讲:图的表示方法
图的表示方法影响算法的效率,常见方法有两种:邻接矩阵 和 邻接表。
2.1 邻接矩阵(Adjacency Matrix)
- 定义:用 矩阵($n$ 为顶点数)表示顶点间的连接关系。
- 无权图:矩阵元素 表示存在边,$0$ 表示不存在。
- 有权图:矩阵元素为边的权重,不存在的边用 表示。
- 示例:无向无权图(顶点 ,边 ):
- 优缺点:
- ✅ 优点:访问速度快(直接查矩阵)。
- ❌ 缺点:存储空间大(稀疏图浪费空间)。
2.2 邻接表(Adjacency List)
- 定义:每个顶点对应一个列表,存储其相邻顶点或边。
- 无权图:列表存储相邻顶点索引(如 表示顶点0相邻顶点1和2)。
- 有权图:列表存储边对象(包含目标顶点和权重)。
实现方式(Java示例):
CodeBlock Loading...优缺点:
- ✅ 优点:节省空间(仅存储存在的边)。
- ❌ 缺点:访问相邻顶点需遍历列表。
练习2:用邻接表表示有向有权图(顶点 权重5,$B(1) \to C(2)$ 权重3):
第三讲:图的遍历算法
遍历图的目的是访问每个顶点一次,常见算法有 深度优先搜索(DFS) 和 广度优先搜索(BFS)。
3.1 深度优先搜索(DFS)
- 核心思想:从起点出发,尽可能深入访问相邻顶点,遇无法继续则回溯。
- 实现方式:递归或栈(此处用递归)。
- 算法步骤:
- 标记当前顶点为已访问。
- 递归访问所有未访问的相邻顶点。
- 示例(无向图 ):
- 起点0 → 访问0 → 访问1 → 访问2 → 访问3(假设邻接顺序为0→1→2→3)。
代码框架(Java):
CodeBlock Loading...
3.2 广度优先搜索(BFS)
- 核心思想:从起点出发,逐层访问相邻顶点(类似“水波扩散”)。
- 实现方式:队列(FIFO)。
- 算法步骤:
- 将起点入队,标记为已访问。
- 取出队首顶点,访问其所有未访问的相邻顶点,标记并加入队列。
- 示例(无向图 ):
- 起点0 → 队列:[0] → 访问0,相邻顶点1入队 → 队列:[1] → 访问1,相邻顶点2入队 → 队列:[2] → 访问2,相邻顶点3入队 → 访问3。
代码框架(Java):
CodeBlock Loading...
练习3:画出以下图的DFS和BFS遍历顺序(起点0):
- DFS顺序:0 → 1 → 2 → 3 → 4
- BFS顺序:0 → 1 → 3 → 2 → 4
第四讲:有权图与经典算法
4.1 最小生成树(MST)
- 定义:连通有权图中,边权和最小的生成树。
- 应用场景:构建最低成本的网络(如城市间管道铺设)。
- 算法:
- Prim算法:从顶点出发,每次选当前生成树与未加入顶点的最小边(适用于稠密图)。
- Kruskal算法:从边出发,按权重排序后选最小边,用并查集避免成环(适用于稀疏图)。
4.2 最短路径算法(Dijkstra)
- 定义:求单源顶点到其他所有顶点的最短路径(边权非负)。
- 应用场景:导航系统(如最短驾车路线)。
- 核心思想:维护每个顶点到源点的当前最短距离,逐步更新(贪心策略)。
示例:源点0到顶点3的最短路径:
CodeBlock Loading...
Lecture 11 - Binary Search Trees
一、二叉树基础
1. 二叉树定义
- 结构:要么为空,要么由根节点、左子树、右子树组成。
- 节点:每个节点包含一个元素(
element),以及左右子节点指针(left/right)。 - 术语:
- 叶子节点:没有子节点的节点(如示例中的45、57、67、107)。
- 子树:节点的左/右分支形成的树。
2. 二叉树的表示(Java代码)
- 每个节点是一个泛型类,可存储任意类型数据。
二、二叉搜索树(BST)核心性质
1. BST的特性
- 无重复元素(默认)。
- 有序性:
- 左子树所有节点的值 小于 根节点的值。
- 右子树所有节点的值 大于 根节点的值。
- 示例:
60 / \ 55 100 / \ / \ 45 57 67 107- 55 < 60 < 100,45 < 55 < 57,67 < 100 < 107,符合BST规则。
2. BST的优势
- 高效搜索、插入、删除(理想情况下时间复杂度为 )。
- 中序遍历有序:遍历结果为升序序列(如上述示例中序遍历结果:45, 55, 57, 60, 67, 100, 107)。
三、BST核心操作:插入元素
1. 插入逻辑
- 树为空:直接创建根节点。
- 树非空:
- 用
current和parent指针找到插入位置:- 若插入值 < 当前节点值,移动到左子树;
- 若插入值 > 当前节点值,移动到右子树;
- 若相等(重复),插入失败。
- 找到
parent后,根据值大小决定插入左或右子节点。
- 用
2. 代码实现(Java)
3. 示例:插入101到BST
- 步骤跟踪:
- 从根节点60开始,101 > 60 → 移动到右子节点100。
- 101 > 100 → 移动到右子节点107。
- 101 < 107 →
parent=107,current=null,插入到107的左子节点。
- 结果:107的左子节点为101,树结构保持BST性质。
四、BST核心操作:搜索元素
1. 搜索逻辑
- 从根节点开始,逐层比较:
- 若搜索值 < 当前节点值,搜索左子树;
- 若搜索值 > 当前节点值,搜索右子树;
- 相等则返回
true,遍历完仍未找到则返回false。
2. 代码实现(Java)
3. 示例:搜索57
- 60 → 55(左子节点)→ 57(右子节点),找到,返回
true。
五、树的遍历
1. 深度优先遍历(递归实现)
- 前序遍历(根→左→右):访问根节点,递归遍历左子树,递归遍历右子树。
- 中序遍历(左→根→右):BST中序遍历结果为升序序列(重点!)。
- 后序遍历(左→右→根):常用于删除树前释放资源。
2. 代码示例(中序遍历)
3. 广度优先遍历(层序遍历)
- 按层访问:根节点→第一层子节点→第二层子节点→…
- 实现:使用队列,先入队根节点,每次出队一个节点,入队其左右子节点(若存在)。
好的!我们继续学习剩余内容,包括 BST删除操作、迭代器实现、Huffman编码 和 时间复杂度分析。
六、BST核心操作:删除元素
删除操作是BST中最复杂的操作,需分情况处理,确保删除后仍满足BST性质。
1. 准备工作:定位节点
- 用
current指向待删除节点,parent指向其父节点。 - 若
current为null,说明元素不存在,删除失败。
2. 情况1:当前节点无左子树
- 操作:直接用当前节点的右子树替代当前节点。
示例:删除节点10(父节点20):
CodeBlock Loading...代码逻辑:
CodeBlock Loading...
3. 情况2:当前节点有左子树
- 核心思想:找到当前节点左子树中的 最右节点(rightMost),用其值替换当前节点的值,然后删除该最右节点(因其无右子树,转化为情况1)。
- 原因:rightMost是左子树中最大的值,替换后不会破坏BST的有序性。
- 步骤:
- 找到
current左子树的最右节点rightMost及其父节点parentOfRightMost。 - 用
rightMost的值覆盖current的值。 - 删除
rightMost(若rightMost有左子树,连接到parentOfRightMost的右侧)。
- 找到
示例:删除节点20(左子树最右节点为16):
CodeBlock Loading...代码逻辑:
CodeBlock Loading...
七、使用迭代器遍历树
1. 为什么需要迭代器?
- 传统的
inorder()、preorder()方法只能打印元素,而迭代器允许通过foreach循环灵活处理元素(如转换为大写、过滤等)。
2. 实现步骤(以中序遍历为例)
- 定义迭代器类:实现
java.util.Iterator接口。 - 中序遍历存储元素:在迭代器构造函数中执行中序遍历,将元素存入列表
list。 - 实现迭代器方法:
hasNext():检查列表是否有剩余元素。next():返回当前元素并移动指针。remove():删除当前元素(需重新构建列表)。
3. 代码示例
4. 使用示例
八、Huffman编码:二叉树在数据压缩中的应用
1. 核心思想
- 用 频率高的字符分配短编码,频率低的分配长编码,减少整体编码长度。
- 通过构建Huffman树(带权路径长度最短的二叉树)实现。
2. 构建Huffman树的步骤(贪心算法)
- 统计字符频率:例如,字符串"Mississippi"中,'i'和's'频率为4,'M'为1,'p'为2。
- 创建初始森林:每个字符作为一棵单节点树,权重为频率。
- 合并最小权重树:
- 每次选取权重最小的两棵树,创建父节点,权重为两者之和。
- 重复直到只剩一棵树(Huffman树)。
3. 生成字符编码
- 从根节点到叶子节点的路径中,左分支记为
0,右分支记为1。 示例:
CodeBlock Loading...编码:M=000, p=001, s=01, i=1
4. 代码实现(简化版)
九、时间复杂度分析
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 插入/搜索/删除 | (h为树高) | 最坏情况树退化为链表,$h=n$,$O(n)$;理想情况平衡树,$h=\log n$,$O(\log n)$ |
| 遍历(前/中/后序) | 每个节点访问一次 |
十、课程总结
1. 核心知识点
- BST性质:左<根<右,中序遍历有序。
- 三大操作:
- 插入:找到合适位置,保持BST性质。
- 搜索:逐层比较,高效查找。
- 删除:分情况处理,维护BST结构。
- 遍历方式:前序/中序/后序(递归)、层序(队列)。
- 迭代器:通过中序遍历实现,支持灵活遍历。
- Huffman编码:利用二叉树压缩数据,贪心算法构建最优树。
2. 代码结构建议
- TreeNode:作为内部类,封装节点属性。
- BST类:实现插入、删除、搜索、遍历、迭代器等方法。
- Huffman相关类:独立实现树构建和编码生成。
3. 练习建议
- 手动绘制:删除BST中的节点,如删除示例树中的60,观察结构变化。
- 编码实践:实现Huffman编码的完整流程(编码+解码)。
- 扩展思考:如何优化BST的性能?(提示:平衡树如AVL、红黑树)
课后问题
- 删除BST节点时,为什么选择左子树的最右节点替换,而不是右子树的最左节点?
- Huffman树中,为什么合并最小权重的两棵树能得到最优编码?
- 尝试用迭代方式实现前序遍历(非递归)。
Lecture 12 - AVL Trees
一、AVL树核心知识
1. 为什么需要AVL树?
- 问题:普通二叉搜索树(BST)在最坏情况下高度为O(n),导致搜索、插入、删除的时间复杂度退化为O(n)。
- 目标:通过保持树的“平衡”,使高度近似O(log n),从而保证操作的高效性。
- 平衡定义:每个节点的左右子树高度差(平衡因子)绝对值不超过1。平衡因子 = 右子树高度 - 左子树高度。
2. AVL树的平衡操作(四种旋转)
当插入或删除节点导致某节点平衡因子为±2时,需要通过旋转重新平衡。以下是四种旋转的核心逻辑:
LL旋转(左左失衡,右单旋)
- 场景:节点A左子树高度比右子树高2,且左子节点B是左重(平衡因子≤0)。
- 操作:将B提升为新根,A变为B的右子节点,B原来的右子树T3变为A的左子树。
示例:
CodeBlock Loading...
RR旋转(右右失衡,左单旋)
- 场景:节点A右子树高度比左子树高2,且右子节点B是右重(平衡因子≥0)。
- 操作:将B提升为新根,A变为B的左子节点,B原来的左子树T2变为A的右子树。
示例:
CodeBlock Loading...
LR旋转(左右失衡,先左旋B再右旋A)
- 场景:节点A左子树高度比右子树高2,且左子节点B是右重(平衡因子+1)。
- 操作:先对B进行左旋转,再对A进行右旋转,中间节点C成为新根。
示例:
CodeBlock Loading...
RL旋转(右左失衡,先右旋B再左旋A)
- 场景:节点A右子树高度比左子树高2,且右子节点B是左重(平衡因子-1)。
- 操作:先对B进行右旋转,再对A进行左旋转,中间节点C成为新根。
示例:
CodeBlock Loading...
3. AVL树的实现关键点
- 节点设计:每个节点包含高度属性,用于计算平衡因子。
- 插入/删除流程1
- 按BST规则插入/删除节点。
- 从插入/删除节点向上遍历,更新路径上所有节点的高度。
- 检查每个节点的平衡因子,触发对应的旋转操作。
代码示例(插入后的平衡处理):
CodeBlock Loading...
4. 时间复杂度分析
- 树的高度为O(log n),平衡操作(旋转)是常数时间,因此插入、删除、搜索的时间复杂度均为O(log n)。
二、哈希表核心知识
1. 什么是哈希表?
- 目标:通过哈希函数将键映射到数组索引,实现O(1)平均时间复杂度的查找、插入、删除。
- 核心组件:
- 哈希函数:将键转换为哈希码(整数),再压缩为数组索引(如
hashCode % N,N为表大小)。 - 碰撞处理:当不同键映射到同一索引时,通过开放寻址法或链地址法处理。
- 哈希函数:将键转换为哈希码(整数),再压缩为数组索引(如
2. 哈希函数与哈希码
- 哈希码计算:
- 基本类型:直接转换(如int类型直接作为哈希码)。
- 长类型/双精度:通过高32位和低32位异或(XOR)避免信息丢失。
- 字符串:通过多项式哈希(如
hash = 31 * hash + charValue)。
- 压缩方法:使用取模运算
hashCode % N,N通常为质数以减少碰撞。
3. 碰撞处理方法
开放寻址法:在数组中寻找下一个可用位置。
- 线性探测:依次检查下一个位置(
(index + 1) % N,(index + 2) % N...),易形成“聚类”。 - 二次探测:检查
(index + j²) % N(j=1,2,3...),减少聚类但可能导致“二次聚类”。 - 双重哈希:使用第二个哈希函数确定步长(如
h'(key) = 7 - key % 7),步长需与N互质。
链地址法(分离链接):每个索引对应一个链表(或链表+红黑树,如Java 8后的HashMap)。
- 优点:处理碰撞简单,无负载因子上限。
- 缺点:链表过长时查找效率下降,需结合负载因子扩容。
4. 负载因子与重新哈希
- 负载因子:
λ = 元素数 / 表大小,开放寻址法通常需λ<0.5,链地址法λ<0.9。 - 重新哈希:当λ超过阈值时,增大表大小(如翻倍),重新计算所有键的索引,避免性能下降。
5. MyHashMap实现关键点
- 哈希函数优化:通过位移和异或(如Java的
supplementalHash)让哈希码更均匀分布。 - 链地址法实现:使用数组存储链表,插入时检查键是否存在,存在则更新值,否则添加到链表尾部。
代码示例(哈希函数):
CodeBlock Loading...
三、学习建议
- AVL树部分:
- 手动绘制插入/删除示例(如文档中的插入25,20,5,34...的过程),观察旋转如何恢复平衡。
- 编写旋转函数时,先理清节点指针的变化(父节点、左右子树的连接),再处理高度更新。
- 哈希表部分:
- 对比不同碰撞处理方法的优缺点,思考为何Java的HashMap在链长较长时转为红黑树。
- 实现MyHashMap时,注意负载因子的阈值设置和重新哈希的时机,避免频繁扩容。
- 实践步骤:
- 先完成AVL树的节点定义、插入/删除的BST部分,再逐步添加平衡逻辑。
- 哈希表从简单的链地址法开始,实现put、get、containsKey方法,再考虑扩容和性能优化。
四、课后小问题(检验理解)
- AVL树中,LL旋转和LR旋转的区别是什么?什么场景下触发?
- 哈希表中,为什么负载因子过高会影响性能?重新哈希的代价是什么?
- 链地址法和开放寻址法在删除操作上有何不同?(提示:开放寻址法删除需标记“墓碑”)