My Introduction
CPT108,23-24开设的新课程数据结构与算法。
Programmes on which the module is shared:
BEng Computer Science and Technology
这是西交利物浦大学计算机科学与技术大二下册的课程笔记以及课程发布过的材料。
本课程设计之初对于学生能力的期望过高,在开课前几周进行了调整,将有难度的课题调整为大量的选学/不学内容。在教学上,能简就简,富有专业特色。导致在很多细节上不如国内的课本详实。本笔记的目标是不仅掌握课程内容,还尽量完成在课程中出现的所有选学内容。
简单回忆一下Final Exam,并没有难题,对概念考察也没有非常细致,1-15选择题30分,16-22问答题70分。问答题全面考了从递归开始后面每一章的内容。文字叙述最多的题目BFS和DFS的区别在上课也详细讲解了,在本笔记当然也提及到了。只要认真学过一遍,就分数就不是问题。
这个网页里的内容是我直接照搬的,还有复习页和对应知识点的超链接标识,由于过于庞大懒得修改了,认真读见以下渠道。
本笔记及系列文件已上传至https://github.com/fancilonely/CPT108_23_24 的github项目,感兴趣请前往github查看。
我的博客也有对应页CPT108 Data Structure and Algorithms_XJTLU_CST笔记 – 梦幻空白~Fancilonely's world
Onedrive 文件链接
百度网盘 文件链接
by FancyTowa
2024/7/16
Contents
My Introduction........................................... 1
Contents.................................................. 2
Lecture 0 Course Introduction and Logistics .............. 8
What is this course about?............................ 8
What is this course NOT about?........................ 9
You preperation...................................... 10
Laboratories ......................................... 10
Tools Required ...................................... 10
Assessment .......................................... 11
Lecture 1 Problem Analysis and Procedural Abstraction .... 11
Problem Solving and Program Development ............. 11
Good Programming practicing & Bad Programming Practice ........................................ 11
Example课程里使用的示例 Java美元换人民币的程序制作 ........... ............................. 12
Program Debugging and Testing ....................... 13
Review:Sources of errors ........................ 13
Program Debugging and Testing(cont.) ............. 14
Alternate Program Design ......................... 14
Procedural Abstraction Why? ...................... 14
Data Types an Scope of Variables .................... 15
Data Type ....................................... 16
Scope of variables ............................... 16
Lecture 2 Optional ...................................... 18
Problem 1 Moving the zero in an array to the last. .. 18
Problem 2: ask whether it is a leap year. ............ 21
Problem 3: the original 2048 small game production. .. 22
Laboratory 0 Treasure Hunter ............................. 24
Breakpoint ......................................... 24
Running the debugger ............................... 25
Lecture 3 Analysis of Algorithms ........................ 26
Formalization of the 1st Algorithm – Linear search . 26
Formalization of the 2nd Algorithm – Binary search .. 27
Some tips on Debugging an Algorithm ................. 28
Lecture 4 Analysis of Algorithms: Asymptotic Notations ... 29
Some tips on Debugging an Algorithm ................. 29
Performance of Different Sorting Algorithms .......... 30
Performance analysis: Running time .............. 30
Performance of Different Sorting Algorithms ...... 31
O “Big-Oh” Notation ............................ 32
Ω “ Big-Omega” Notation ....................... 33
Θ “Big-Theta” Notation .......................... 34
Asymptotic analysis: General Rules .................. 34
Loops (for loop, while loops) ................... 35
Nested loops ..................................... 35
General rules ........................................ 35
Algorithm 1 ..................................... 36
Algorithm 2 ..................................... 36
Problems cont. ................................... 37
Laboratory 1 Complexity analysis Week2 ................... 39
插入排序(Insertion Sort) ............................. 42
堆排序(Heap Sort) .................................... 42
合并排序(MergeSort) .................................. 43
快速排序(Quick Sort) ................................. 45
希尔排序 (Shell Sort) ................................ 46
选择排序 (Selection Sort) ............................ 46
Lecture 5 Data Structures and Abstract Data Type ......... 47
A Use Case: Phone Book .............................. 47
Abstract Data Types ................................. 47
Object Construction and Destruction Constructor and Destructor ........................................ 48
Data-directed design ................................. 50
An example: Counter .................................. 51
value = 0; .......................................... 52
An example: Complex number .......................... 56
Why? ........................................... 58
Lecture 6-7 Data Structures and Abstract Data Type Data Abstraction ................................ 59
Data Abstraction ..................................... 59
Data requirements .................................... 59
Concept of an Object .................................. 60
Levels of Abstraction ................................. 60
Data Abstraction Example: Indexed Set ................ 60
ComplexSet .......................................... 63
Summary ............................................. 65
Lecture 8: Recursion ..................................... 66
Recursion introduction ............................... 66
二分法(Bisection Method) ........................... 66
moveDisks 方法 ................................... 66
Example Hanoi Tower .................................. 67
变量和常量 ....................................... 68
run 方法 ......................................... 68
main 方法 ........................................ 68
The principle of the Hanot Tower ................. 69
Problems faced with using the recursion ............... 69
Laboratory 2 Recursive ................................... 70
Split() .............................................. 72
2.1 split(String regex) ......................... 72
2.2 split(String regex, int limit) .............. 74
Laboratory 2: Extra Part Fibonacci ................... 74
Lecture 9-12 Comparison Based Sorting ................... 75
L9-10 Sorting Basics ................................. 75
Selection sort ....................................... 76
Insertion sort ....................................... 77
Bubble sort .......................................... 78
L11 Sortings ......................................... 80
Merge sort ........................................... 80
Quick sort ........................................... 82
L12 Heap Sort ........................................ 85
引入 ............................................. 85
Array and Binary tree ............................ 85
数组 ............................................. 86
二叉树 ........................................... 86
满二叉树 ......................................... 86
堆(Heap) ....................................... 86
Potential Problems .............................. 87
Maintaining the heap property: Heapify .......... 87
Pseudocode ....................................... 87
注意的细节 ....................................... 90
Deletion ......................................... 91
Tutorial 01 Recursive & Code Tracing ..................... 92
问题1 爬楼梯的递归 ................................... 92
Code Tracing ........................................ 95
问题2 气泡排序问题 ............................... 95
问题3 算法的改进和优化 ........................... 96
问题4 搜索大于目标的第一个数字 ................... 98
附运行实例 ...................................... 100
Assignment1 ............................................. 101
Problem 1 对数运算 .................................. 101
Problem 2 多项式的求值算法 .......................... 101
Problem 3 分而治之 .................................. 102
Problem 4 两种排序的比较 ............................ 103
Problem 5 插入排序 .................................. 104
Problem 6 快速排序 .................................. 104
Problem 7 排序的选用 ................................ 104
Problem 8 Merge sort ................................ 105
Problem 9 递归 ...................................... 105
Problem 10 Heap Sort ................................ 108
Lecture 13 Non-comparison Based Sorting ................. 109
Counting sort ...................................... 109
算法复杂度(时间复杂度) ........................ 111
空间复杂度........................................... 111
一些灵活应用 .................................... 111
优势 ............................................ 111
劣势 ............................................ 112
Radix Sort .......................................... 112
Pseudocode ..................................... 112
复杂度 .......................................... 113
Advantages ...................................... 113
Drawbacks ....................................... 113
Lecture 14 Linked Lists ................................. 114
Disadvantages of Arrays .......................... 114
链表的基础 ........................................ 115
基本类型 ...................................... 115
单链表的四种基本操作 .......................... 115
链表的基本思想 ................................ 115
链表的基础接口 ................................ 117
链表的具体实现 ................................ 118
链表的遍历和搜索 ................................ 119
链表的指定位置插入 ............................ 120
Variants of Linked lists ............................ 120
Circular linked lists ........................... 120
Doubly linked lists ............................. 120
Arrays vs Linked lists .............................. 121
Laboratory 3 Linked Lists ............................... 122
Node.java ........................................... 122
SortedList.java ..................................... 124
SortedLinkedlist.java ............................... 126
Lecture 15 Stack and Queue .............................. 130
Memory model ........................................ 130
Stack ............................................... 130
Stack code .......................................... 131
Stack Applications .................................. 134
左右括号匹配应用 ................................ 134
运算应用 ........................................ 134
Stack Implementation ................................ 135
Queue ............................................... 135
Desirable operations 理想的操作 ..................... 136
Queue Applications ................................ 139
Queue: implementation2 .............................. 140
Questions ........................................... 141
Lecture 16 Trees ....................................... 142
Tree相关定义及解释 .................................. 143
Example: Expression trees 表达式树 ................. 144
Binary tree 二叉树 ................................. 144
二叉树要么为空要么非空 .......................... 144
满二叉树 ........................................ 145
完全二叉树 ...................................... 145
完美二叉树 ...................................... 145
二叉树的高度 ................................... 146
二叉树遍历Tree traversal (cont.) ................ 146
二叉树搜索的缺点 ................................ 147
Binary Search Tree (BST)二叉搜索树 .............. 147
二叉树结构中的元素集合 .......................... 148
例子:Find Min and Max .......................... 150
Lecture 17 Trees Insertion and Deletion ................. 151
Trees Insertion ..................................... 151
Trees Deletion ...................................... 152
Binary search tree (BST) ........................... 152
Successor ...................................... 152
时间复杂度 ..................................... 155
Problems with BSTs ............................ 155
Balanced vs Unbalanced Tree .................... 156
How Fast is Sorting in BST? ..................... 157
Tutorial 02 Stack and Queue & tree ...................... 157
Stack and Queue ................................... 158
Problem 1 Stack&Queue 操作 ...................... 158
Problem 2 Linear time ........................... 160
Tree .............................................. 160
Problem 3 Min-heap .............................. 160
Problem 4 BST ................................... 161
Problem 5 BST ................................... 162
Problem 6 ....................................... 163
Lecture 18-20 Hashtables ................................ 167
L18=L19-20................................................... 167
Hash table 定义 ..................................... 167
Hash table Applications ............................. 168
Hashtables 冲突的处理 ............................... 168
链地址法(Separate Chaining) ...................... 168
开放寻址法(Open Addressing) ................... 170
Hashtable Performance ............................... 174
负载因子(Load Factor) ......................... 174
情况一:固定槽位数量(M),增加元素数量(N) .... 174
情况二:增加槽位数量(M),增加元素数量(N) .... 175
哪个哈希表性能更好? ............................ 175
Time Complexity ................................. 175
Hashtable: Resize .................................. 176
Lecture 21 Graphs ....................................... 176
Graphs Applications ................................. 176
Graphs Formal definitions ........................... 177
1.顶点集合V 边集合E与边的关系 .................. 178
2.边的方向以及环 ................................ 178
3.边的序号和一个顶点的Deg ....................... 178
4.Path路径 ...................................... 178
5.Connectivity连通性 ............................ 179
6.可能有顶点没路径连接到 ........................ 179
Edge ................................................ 179
图的关系表示 ........................................ 180
矩阵表示 ........................................ 180
列表表示 ........................................ 181
Lecture 22-23 GraphsTraversal: BFS and DFS .............. 182
Subgraph ............................................ 182
Traversing a Graph .................................. 183
图遍历基础 ...................................... 183
递归遍历图 ...................................... 183
示例:二叉树与完全二叉树 ........................ 184
BFS ................................................. 184
Applications .................................... 184
原理 ............................................ 184
两种BFS流程的表示法 ............................ 185
Pseudocode ..................................... 186
Recap: Adjacency list vs Adjacency matrix ....... 186
DFS ................................................. 188
Applications .................................... 188
原理 ............................................ 188
两种DFS流程的表示法 ............................ 188
Pseudocode ..................................... 189
DFS(G) .......................................... 190
RDFS(v) ........................................ 190
时间复杂度分析 .................................. 190
Tutorial 03 Hashtables & Graphs ......................... 192
Hashtables .......................................... 192
Problem 1 单链哈希 .............................. 192
Problem 2 开放寻址哈希 .......................... 192
Problem 3 开放寻址哈希 .......................... 194
Graphs .............................................. 195
Problem 4 Graph ................................. 195
Problem 5 Graph表示法 ........................ 195
Problem 6 BFS ................................... 196
Problem 7 Stack & Queue & BFS ................... 197
Final Review ............................................ 199
Problem solving and problem analysis ............... 199
Problem analysis ................................ 199
Program design .................................. 200
Problem solving techniques ..................... 200
Abstract data type (ADT) (Classes) ............. 200
Basics of analysis ............................. 200
Data structures ..................................... 201
Data type ...................................... 201
Abstract data types (ADTs) (Classes) ........... 201
Arrays and linked lists ......................... 201
Trees .......................................... 201
Heaps .......................................... 202
Hashtables ..................................... 202
Algorithms ......................................... 202
Sorting algorithms ............................. 203
Complexities of Different Sorting algorithms ... 203
Graph .......................................... 204
Suggested review methods ............................ 205
My Note Final Part I .................................... 205
My Note Final Part II ................................... 207
<H2>Lecture 0 Course Introduction and Logistics </H2>
<H3>What is this course about?</H3>
▶更多关于组织、访问、管理的基本知识
数据和计算程序来操纵他们
▶触及了……的基本概念的基础
程序设计和算法分析
▶不同类型的数据
▶用于使用集合实现的数据结构
▶算法的复杂性和正确性
▶渐近分析
▶调整阵列大小
▶哈希
▶排序
▶搜索
▶etc
▶具有适当数据结构的算法的实现
<H3>What is this course NOT about?</H3>
▶编程语言
▶算法将使用简单的纯英语或伪代码
▶将伪代码转换成您最喜欢的编程程序集
语言应该是一项相当简单的任务
<H3>You preperation</H3>
必须有编程基础,很酷不教
<H3>Laboratories</H3>
2周2h的lab
<H3>Tools Required </H3>
需要以下对应软件及版本(安装了不同版本很可能会出现不兼容)
1.JDK(学CPT111 肯定安装了)
2.Gradle lab采用Gradle (version 8.5) 下载 具体情况请见Lecture2
3.IDE:Eclipse lab采用2023-12 eclipse迭代很快,基本用最新版
提示:win+R
sysdm.cpl 系统属性(环境变量)
cmd 命令提示符
如果在网上搜索相关教程不了解的,建议参加W1的lab0,让老师和助教手把手教学,很容易就明白了。
<H3>Assessment </H3>
▶学期内评估:30%
▶实验室:18%(6×3%)
▶作业:12% (1-2个作业)
▶期末考试:70%
▶Resit考试:100%
<H2>Lecture 1 Problem Analysis and Procedural Abstraction</H2>
<H3>Problem Solving and Program Development </H3>
Problem Statement问题陈述
Problem Analysis 问题分析
▶ Input
▶ Output
▶ Constraints
▶ Abstraction
(Solution Development 解决方案开发Solution Validation / Verification解决方案验证/验证)
Program Design
▶ Decomposition
▶ Initial Algorithm Development
▶ Algorithm Refinement
Program Implementation (i.e., Coding) 方案实施
▶ Translate the algorithm into program using a programming language
Program Debugging & Testing 程序调试和测试
<H4>Good Programming practicing & Bad Programming Practice</H4>
①Before you code / write the program
1.Think and design carefully
2.Check and prove the algorithm
3.Organize the program such that it can be easy to
understand and debug
②Check your program 检查你的程序
1.Reduce the number of errors
2.Reduce the amount of time spent in debugging
3.Produce GOOD programs
<H4>Example课程里使用的示例 Java美元换人民币的程序制作</H4>
Problem Statement
Problem Analysis
Input:
▶ nickels (US 5 cents) (integer) – count of nickels
▶ pennies (US 1 cents) (integer) – count of pennies
Output:
▶ dollars (integer) – count of Chinese yuan in return
▶ change (integer) – count of 10-cents coins
Constraints: None
Program Design
Knowledge:
▶ one dollar equals 100 cents
Decomposition:
▶ Find the total amount in US dollars
▶ Input: count of nickels and pennies
▶ Output: total_US_dollars
▶ Find the total amount of Chinese yuan in exchange
▶ Input: total_US_dollars
▶ Output: total_CHN_dollars
▶ Find the number of Chinese yuan and 10-cent coins
▶ Input: total_CHN_dollars
▶ Output: Chinese yuan and 10-cents coins change
Program Decomposition
Initial Algorithm:
1. Read in the count of nickels and pennies
2. Compute the total amount in US dollars
▶ total_US_dollars = 5 ∗ nickels + pennies
3. Compute the total amount in Chinese yuan in exchange
total_CHN_dollars = exchange_us2chn(total_US_dollars)
4. Find the value in Chinese yuan and change
▶ total_CHN_cents (integer, total amount of 10-cents
coins)
▶ total_CHN_cents =
total_CHN_dollars ∗ DOLLAR2CENTS
▶ yuan = total_CHN_cents / DOLLAR2CENTS
▶ change = total_CHN_cents % DOLLAR2CENTS
5. Display the value in Chinese yuan and change
Procedural Abstraction
To solve the exchange problem separately:
Function exchange_us2chn()
▶ Input: US_dollars (real) – amount of US dollars
▶ Output: CHN_dollars (real) – amount in Chinese yuan in
exchange
<H3>Program Debugging and Testing </H3>
<H4>Review:Sources of errors </H4>
▶ Program can’t be compiled – syntax error
▶ Careless in coding, relatively easy to fix 编程出错
▶ Program can’t be linked – link error
▶ Forget or use wrong language libraries 错误的语言库
▶ Program is incorrect – run time error
▶ Implementation (coding) error (e.g., wrong output, division by
zero) 错误的输出,溢出等
→ Locate the bugs and fix
▶ Algorithmic error
▶ Should not happen if check and prove carefully before
implementation优先级 逻辑 简单代码 递归等
<H4>Program Debugging and Testing(cont.)</H4>
▶Think and debug program way:TOP-DOWN从上往下的思考方式
▶Pay special attention to INTERFACES between the major
steps/functions 接口之间的关系
▶1. input → nickels, pennies
2. nickels, pennies → total_US_dollars
3. total_US_dollars → total_CHN_dollars
4. total_CHN_dollars → dollars, change
5. dollars, change → output
▶Generate test cases:
Enter the number of nickels and press return: 21
Enter the number of pennies and press return: 105
The change is 18 Chinese yuan and 7 10-cents.
Enter the number of nickels and press return: 3
Enter the number of pennies and press return: 2
The change is 1 Chinese yuan and 2 10-cents.
<H4>Alternate Program Design</H4>
Compute the total amount in Chinese yuan in exchange 简化
total_CHN_dollars = total_US_dollars ∗ US2CHN
<H4>Procedural Abstraction Why?</H4>
1.TOP-DOWN DESIGN
分解为小问题 可以递归可以变简单 可以给任何人在任何时间解决 可以使用新的强大技术
2.complex subproblems
简化不必要的细节 容易理解 容易修改 替换不必要的而不影响主程序代码
3.frequency encountered subproblems
允许代码重用 只有改动部分
package xjtlu.cpt108.basics;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class main {
private static final int DOLLARS2CENTS = 100;
private static final double US2CHN = 7.2;
// Compute the equivalent Chinese yuan from USD
private static double exchange_us2chn(double US_dollars) {
return US2CHN * US_dollars;
}
public static void main(String [] args){
int pennies; // count of pennies
int nickels; // count of nickels
int yuan; // value of coins in Chinese yuan
int change; // value of coins in Chinese 10-cents
double total_US_dollars; // total US dollars
double total_CHN_yuan; // total Chinese dollars
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
try {
// Read in the count of nickels and pennies
System.out.println("Enter the number of nickels and press enter:");
nickels = Integer.parseInt(br.readLine());
System.out.println("Enter the number of pennies and press enter:");
pennies = Integer.parseInt(br.readLine());
// Compute the total amount in US dollars
int total_US_cents = 5 * nickels + pennies;
total_US_dollars = ((double) total_US_cents) / DOLLARS2CENTS;
// Compute the total amount in Chinese dollars using the change rate
total_CHN_yuan = total_US_dollars * US2CHN;
// Find the value in Chinese yuan and change
int total_CHN_cents = (int) (total_CHN_yuan * DOLLARS2CENTS);
yuan = total_CHN_cents / DOLLARS2CENTS;
change = total_CHN_cents % DOLLARS2CENTS / 10;
// Display the value in Chinese yuan adn change
System.out.printf("The change is %d Chinese yuan and %d 10-cents.\n", yuan
, change);
} catch (IOException e) {
e.printStackTrace();
}
}
}
<H3>Data Types an Scope of Variables </H3>
<H4>Data Type </H4>
▶An elementary data abstraction
▶Captures the very nature of the data being considered
▶A special type
Void 空类型
▶Type consistency
variable type and value must be consistent 变量的类和值必须保持一致
▶Arithmetic rule算术规则
operand types | result type |
int, int | int |
real, int | real |
real, real | real |
▶Type Conversion (casting)转换类型 例如
total_US_dollars = ((double) total_US_cents) / DOLLARS2CENTS;
(int) (total_CHN_yuan * DOLLARS2CENTS);
▶Constant Declaration
static final type variablelist
static final int DOLLARS2CENTS = 100;
static final double US2CHN = 7.2;
▶Variable Declaration
type variablelist
int nickels, pennies;
double total_US_dollars;
char char1;
String str;
<H4>Scope of variables</H4>
▶this
The Implicit this class pointer
▶Every class object has an implicit pointer variable this
▶this is the address of the object i.e. this points to the object itself
Typically, we will drop the this except when needed
ie, instead of calling this. setData (int), we simply use setData(int).
publlc class Node {
private int data;
public Node(int data){
setData (data);
}
/*how to save the value to here?*/
public void setData(int data){
this.data = data;
}
}
whats happening here?▶four different access levels
Modifier | Package | Subclass | Public |
public | Yes | Yes | Yes |
protected | Yes | Yes | No |
no modifier | Yes | No | No |
private | No | No | No |
Analysis of Algorithms
<H2>Lecture 2 Optional </H2>
三个问题的解答,考察算法逻辑。
注意这里要正确安装eclipse并学会编译步骤。
*gradlew compileJava/run问题
Gradle 项目打开自动下载8.5-Zip问题及相关配置,
课程建议参考以下方案
1.在文件管理器窗口启动cmd,
2.输入gradle compileJava(不用加w)进行编译
3.输入gradle run进行运行
gradle在本课程是用来离线检查学生的代码是否正确,不需要了解gradle的文件具体含义。
用法是右键文件资源管理器中的test文件。
所以,不要擅自修改test文件。
查看JUnit Test窗口,可以了解自己哪一步出现了问题。
第一个问题,将一个数组中的零移动到最后。
<H3>Problem 1 Moving the zero in an array to the last. </H3>
Task 1
In this exercise, given an array of integers, you are required to write a Java method to rearrange the integers so that all zeros are moved to the end of the array. Further, the order of the non-zero elements must be retained.
For example, given an integer array:
92, 0, 45, 0, 91, 80
After rearrange its elements, the array should look like:
92, 45, 91, 80, 0, 0
Task2
Write a function to generate an array of integers with a given size, and test your function in the
previous task with this function.
Task 1 写了个输入就可以进行movezerotoend
package movezerotoend;
import java.util.Arrays;
import java.util.Scanner;
public class RearrangeArray {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// Prompt the user to enter the array elements separated by commas
System.out.println("Enter the array elements separated by commas: ");
String input = scanner.nextLine();
// Split the input string into an array of strings
String[] elements = input.split(",");
// Create an array and parse the input elements into integers
int[] arr = new int[elements.length];
for (int i = 0; i < elements.length; i++) {
arr[i] = Integer.parseInt(elements[i].trim());
}
rearrangeArray(arr);
System.out.println("After rearranging: " + Arrays.toString(arr));
}
public static void rearrangeArray(int[] arr) {
int index = 0;
// Traverse the array
for (int i = 0; i < arr.length; i++) {
// If the current element is non-zero, move it to the front of the array
if (arr[i] != 0) {
arr[index++] = arr[i];
}
}
// Fill the remaining elements with zeros
while (index < arr.length) {
arr[index++] = 0;
}
}
}
Task2写了个随机生成一个数组,然后自动movezerotoend
package movezerotoendtask2;
import java.util.Random;
public class TestDataGeneration {
public static void main(String[] args) {
// Test the function with array size random
double random = 10*Math.random()+1;
int randomnum = (int)random;
int[] testData = generateIntArray(randomnum);
System.out.println("Generated Array:");
printArray(testData);
// Test the previous task with the generated array
rearrangeArray(testData);
System.out.println("After rearranging:");
printArray(testData);
}
public static int[] generateIntArray(int size) {
int[] array = new int[size];
Random random = new Random();
for (int i = 0; i < size; i++) {
array[i] = random.nextInt(10); // Generates random integers between 0 and 10
}
return array;
}
public static void printArray(int[] array) {
for (int num : array) {
System.out.print(num + " ");
}
System.out.println();
}
public static void rearrangeArray(int[] arr) {
int index = 0;
for (int i = 0; i < arr.length; i++) {
if (arr[i] != 0) {
arr[index++] = arr[i];
}
}
while (index < arr.length) {
arr[index++] = 0;
}
}
}
<H3>Problem 2: ask whether it is a leap year.</H3>
package leapyearchecker;
import java.util.Scanner;
public class LeapYearChecker {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("Enter the array elements separated by commas: ");
String year1 = scanner.nextLine();
int year=Integer.parseInt(year1);
boolean isLeapYear = isLeapYear(year);
if (isLeapYear) {
System.out.println(year + " is a leap year.");
} else {
System.out.println(year + " is not a leap year.");
}
}
public static boolean isLeapYear(int year) {
// Rule 1: If the year is evenly divisible by 4, go to Rule 2; otherwise, it is not a leap year.
boolean divisibleBy4 = (year % 4 == 0);
if (!divisibleBy4) {
return false;
}
// Rule 2: If the year is evenly divisible by 100, go to Rule 3; otherwise it is a leap year.
boolean divisibleBy100 = (year % 100 == 0);
if (!divisibleBy100) {
return true;
}
// Rule 3: If the year is evenly divisible by 400, then it is a leap year; otherwise, it is not a leap year.
boolean divisibleBy400 = (year % 400 == 0);
if (!divisibleBy400) {
return false;
}
return false;
}
}
<H3>Problem 3: the original 2048 small game production.</H3>
<H4>3 The Trap class</H4>
import java.util.Arrays;import java.util.Random;
public class NumberListGenerator {
// Method to generate an ascending list of numbers
public static int[] generateAscendingList(int size) {
int[] ascendingList = new int[size];
for (int i = 0; i < size; i++) {
ascendingList[i] = i + 1;
}
return ascendingList;
}
// Method to generate a descending list of numbers
public static int[] generateDescendingList(int size) {
int[] descendingList = new int[size];
for (int i = 0; i < size; i++) {
descendingList[i] = size - i;
}
return descendingList;
}
// Method to generate a list of randomly generated numbers
public static int[] generateRandomList(int size, int range) {
int[] randomList = new int[size];
Random random = new Random();
for (int i = 0; i < size; i++) {
randomList[i] = random.nextInt(range) + 1;
}
return randomList;
}
// Method to generate a list with repeated items in different patterns
public static int[] generateRepeatedList(int size, int[] pattern) {
int[] repeatedList = new int[size];
int patternLength = pattern.length;
for (int i = 0; i < size; i++) {
repeatedList[i] = pattern[i % patternLength];
}
return repeatedList;
}
public static void main(String[] args) {
int size = 10;
int[] ascendingList = generateAscendingList(size);
System.out.println("Ascending List: " + Arrays.toString(ascendingList));
int[] descendingList = generateDescendingList(size);
System.out.println("Descending List: " + Arrays.toString(descendingList));
int range = 5; // Change the range as needed
int[] randomList = generateRandomList(size, range);
System.out.println("Random List: " + Arrays.toString(randomList));
int[] pattern = {1, 2, 3}; // Change the pattern as needed
int[] repeatedList = generateRepeatedList(size, pattern);
System.out.println("Repeated List: " + Arrays.toString(repeatedList));
}
}
<H2>Laboratory 0 Treasure Hunter</H2>
本实验的目的是教授两个调整测试代码的方法, 一个variable一个expression, 以及学习断点功能.
不过由于是预备实验,并不是必选功能。并不会专门讲解,有问题可以自行网上学习。
<H3>Breakpoint</H3>
<H3>Running the debugger</H3>
上边栏调出显示
在使用debugger的时候,可以从断点处一步步运行,同时查看Variables数值的变化,获取信息。当然,可以从debugger窗口直接跳到对应步骤。
第一个密码
variables视图可以直接获得
第二个密码
有嵌套的if,先variables视图找到长度,表现了长度。
在expression视图里用tostring方法获得字符串。经过一些奇异的调试,找到了对应字符串的调取方法:correctPasscode.stream().map(Object::toString).collect(Collectors.joining())
第三个密码
是一个int类型,需要在断点的while循环对应次数下查询到数值。
密码如下
<H2>Lecture 3 Analysis of Algorithms </H2>
<H3>Formalization of the 1st Algorithm – Linear search </H3>
第一种算法的形式化-线性搜索
Function: linear_search
Input : num
: Value to be found
lockers[n] : An array with size n
Output : true if num is in the lockers; false otherwise
例子是有几个盒子,每个盒子里有不同数字,要找到一个特定数字。
方法就是给盒子标序号 lockers[n] ,每次n+1
依次检查是否是该数字,不是
For each locker from left to right
If the number that we look for is inside the locker
Return true
Return false
1 for i = 0 to n − 1
2 if num == lockers[i]
3 return TRUE
4 return FALSE
<H3>Formalization of the 2nd Algorithm – Binary search</H3>
第二种算法的形式化-二进制搜索
Function: binary_search
Input : num
: Value to be found
lockers[n] : An array with size n
Output : true if num is in the lockers; false otherwise
1 if no locker left
2 return FALSE
3 if num == lockers[middle]
4 return TRUE
5 else if num < lockers[middle]
6 return SEARCH lockers[0]
through lockers[middle − 1]
7 else if num > lockers[middle]
8 return SEARCH lockers[middle + 1]
through lockers[n − 1]
从一半开始,在某1/2吗,在某1/4吗,直到得出结果。
<H3>Some tips on Debugging an Algorithm </H3>
Similar to debug a program, you can:
· Check whether all variables has been defined and initialized
correctly (if necessary)
检查所有变量是否已正确定义和初始化(如有必要)
· Check whether all the null value has been handled
correctly (if necessary)
检查是否已正确处理了所有的空值(如有必要)
· Check whether all cases of a variable, or value returned by
a function, are handled correctly
检查函数返回的变量或返回的值的所有情况是否得到正确处理
· Do the signatures of functions defined and used
consistently?
定义和使用的函数的签名是否一致
<H2>Lecture 4 Analysis of Algorithms: Asymptotic Notations</H2>
<H3>Some tips on Debugging an Algorithm </H3>
Algorithm Analysis
Analyse algorithms can help us:
· To understand the performance of an algorithm
了解一个算法的性能
· To understand the differences between different algorithms
来了解不同算法之间的差异
· To eliminate bad algorithms earlier
· To pinpoints the bottlenecks, which are worth coding
carefully
· To predict how much resources that the algorithm is needed,
为了预测该算法需要多少资源,
e.g.:
· Computational time
· Memory
· Development cost
· Communication bandwidth, if required
· etc.
Different approaches
· Empirical
· Run an implemented systemed on real-world data. Notion of
benchmarks.
· Simulational
· Run an implemented systemed on simulated data.
· Analytical
· Use theoretic-model data with a theoretical model system.
This is what we do in CPT108!
<H3>Performance of Different Sorting Algorithms</H3>
<H4>Performance analysis: Running time </H4>
Running time
这些算法的效率如何?
我们如何衡量它们?关于这个例子可以看lab1的内容。
What is the efficiency of these algorithms? ·
How do we measure them?
<H4>Performance of Different Sorting Algorithms</H4>
Asymptotic notations
· To establish a relative order among functions for large data
size, n
· Commonly used notations in measuring computational time
complexities include:
· Upper bound O(g(n)) 表示最坏情况下的时间复杂度。假设我们使用线性搜索算法,在最坏情况下,我们可能需要遍历整个链表来找到目标元素。因此,时间复杂度可以表示为O(n),即算法的运行时间与链表中元素的数量成正比。
· Lower bound Ω(g(n)) 表示最佳情况下的时间复杂度。如果我们在链表的第一个位置找到目标元素,那么算法的运行时间将是常数级的,与链表中元素的数量无关。因此,最佳情况下的时间复杂度可以表示为Ω(1)。
· Tight bound Θ(g(n))提供了时间复杂度的严格界定,考虑了最坏情况和最佳情况。对于线性搜索算法来说,在最坏情况下,它需要遍历整个链表,而在最佳情况下,它只需检查第一个元素。因此,算法的时间复杂度为Θ(n),因为它在最坏情况和最佳情况下都是线性的。
<H4>O “Big-Oh” Notation </H4>
当谈到算法的时间复杂度时,"大O"(O)符号用于表示最坏情况下算法的上界。换句话说,它描述了算法的运行时间如何随着输入的增加而增加。例如,如果一个算法的时间复杂度为O(n),则表示算法的运行时间最多是输入规模n的某个常数倍。大O符号是一种渐进记法,用于描述算法的性能随着输入规模的增长而如何变化。
· A asymptotic notation used to measure the time taken to run
an algorithm as the size of the problem increase
一种用于测量运行所需时间的渐近符号
随着问题规模的增大,算法采用了一种算法
· i.e., it calculates the worse-case running time (upper bound)
of an algorithm
也就是说,它计算了最坏情况下的运行时间(上限)
算法的
O Notation (cont.)
Major categories of algorithms:
· O(1) – Constant of steps or time
步数或时间的常数
· O(log2n) (or O(lg n), O(log n)) – Logarithmic
对数的
· O(n) – Linear
线性的
· O(n^k) – Polynomial
· O(2^n) – Exponential – intractable
<H4>Ω “ Big-Omega” Notation </H4>
"大Ω"(Omega)符号用于表示算法的最低下界。它描述了在最好情况下算法的运行时间如何随着输入的增加而增加。如果一个算法的时间复杂度为Ω(n),则表示算法的运行时间至少是输入规模n的某个常数倍。换句话说,Omega表示了算法的最佳情况。
· A asymptotic notation used to measure the time complexity
of an algorithm in its best-case. Similarly, we have the following major categories:
· Ω(1)
· Ω(log2n) (or Ω(lg n), Ω(log n))
· Ω(n)
· Ω(n^k)
· Ω(2^n)
<H4>Θ “Big-Theta” Notation </H4>
"大Θ"(Theta)符号用于表示算法的严格上下界。它描述了算法的运行时间在最坏情况和最佳情况下的增长情况。如果一个算法的时间复杂度为Θ(n),则表示算法的运行时间在最坏情况和最佳情况下都是输入规模n的某个常数倍。因此,Theta提供了对算法性能的更严格的界定,同时考虑了最坏情况和最佳情况下的运行时间
· Used to specify the asymptotic bounds (both upper and
lower bounds) of an algorithm
· The bound is the tightest possible
· i.e., the case when the upper bound (O) and the lower
bound (Ω) are equals
and again, we have the following major categories:
· Θ(1)
· Θ(log2n) (or Θ(lg n), Θ(log n))
· Θ(n)
· Θ(n^k)
· Θ(2^n)
<H3>Asymptotic analysis: General Rules </H3>
When T(n) is a polynomial of degree k,
i.e., T(n) = ckn k + ck−1n k−1 + · · · + c0,
therefore T(k) = O(n k )
· If T(n) is a logarithmic function,
i.e., T(n) = log n,
therefore T(n) = O(log n)
<H4>Loops (for loop, while loops) </H4>
· At most the running time of the statements inside the loop × the number of iterations
· O(n)
for (i=0; i<n; i++) {
// do something with O(1)
}
<H4>Nested loops</H4>
· Running time of the statement equals to the product fo the sizes of all loops
· O(mn), or O(n^2) if m = n
<H3>General rules</H3>
time算操作次数来算时间
定义不算,声明都算。
Given a set of N numbers, determine the k th largest value,
where k ≤ N. 找数组中第k大的元素
<H4>Algorithm 1 </H4>
· Read N numbers into an array
· Sort the array in decreasing order by some algorithm
(you can assume it as O(n log n))
· Return the element in position k
1.将N个数字读入一个数组。
2.使用某个O(n log n)的算法按递减顺序对数组进行排序。
3.返回数组中第k个位置上的元素作为答案。
<H4>Algorithm 2 </H4>
· Read the k elements in the array and sort them in
decreasing order
· Each remaining element is read one-by-one
· If smaller than the k th element, then it is ignored
· Otherwise, it is placed in its correct sport in the array,
bumping one element out of the array
· The element in the k th position is returned as the answer
1.从数组中读取前k个元素,并按递减顺序对它们进行排序。
逐个读取剩余的元素。
2.如果一个元素小于第k个元素,则忽略它。
否则,将它放置在数组的正确位置,将数组中的一个元素挤出。
3.返回数组中第k个位置上的元素作为答案。
<H4>Problems cont.</H4>
Which algorithm is better when
1. n = 100 and k = 100?
2. n = 100 and k = 1?
· What happens when
3. n = 1,000,000 and k = 500,000?
Let's analyze the two algorithms for different values of n and k:
让我们分析两个算法对于不同的n和k值:
①When n = 100 and k = 100:
当n = 100,k = 100时:
1.Algorithm 1: In this case, both n and k are relatively small. Sorting the entire array (Algorithm 1) might be efficient, and the overhead of sorting is not significant for a small array size.
算法1:在这种情况下,n和k都相对较小。对整个数组进行排序(算法1)可能是有效的,并且对于小的数组大小,排序的开销并不显著。
2.Algorithm 2: Partial sorting (Algorithm 2) might still be reasonable, but it involves sorting only the first k elements. However, considering the small values of n and k, the difference in performance might not be substantial.
算法2:部分排序(算法2)可能仍然是合理的,但它只涉及前k个元素的排序。然而,考虑到n和k的值较小,性能上的差异可能并不显著。
Conclusion: Algorithm 1 and Algorithm 2 might have comparable performance, but Algorithm 1 might be slightly more straightforward.
结论:算法1和算法2可能具有相当的性能,但算法1可能稍微简单一些。
②When n = 100 and k = 1:
当n = 100且k = 1时:
1.Algorithm 1: Again, with such small values of n and k, sorting the entire array is not a significant overhead. Algorithm 1 can efficiently find the largest element by directly accessing the first element after sorting.
算法1:同样,对于如此小的n和k值,对整个数组进行排序并不是一个很大的开销。算法1可以通过直接访问排序后的第一个元素来有效地找到最大的元素。
2.Algorithm 2: In this case, Algorithm 2 would essentially sort only the first element, which is not a substantial advantage over Algorithm 1.
算法2:在这种情况下,算法2基本上只对第一个元素进行排序,这与算法1相比并没有实质性的优势。
Conclusion: Algorithm 1 is likely more efficient for finding the largest element in this scenario.
结论:算法1可能更有效地找到这种情况下的最大元素。
③When n = 1,000,000 and k = 500,000:
当n = 1,000,000且k = 500,000时:
1.Algorithm 1: Sorting the entire array (O(n log n)) might become computationally expensive for such a large value of n.
算法1:对整个数组进行排序(O(n log n))对于如此大的n值可能会变得计算昂贵。
2.Algorithm 2: Partial sorting the first k elements (O(k log k)) may be more efficient since k is significantly smaller than n. However, the insertion of elements one by one could still result in a substantial time complexity.
算法2:对前k个元素进行部分排序(O(k log k))可能更有效,因为k明显小于n。然而,逐个插入元素仍然可能导致相当大的时间复杂度。
Conclusion: Algorithm 2 might be more favorable for this scenario, especially considering the smaller value of k.
结论:算法2可能更适合这种情况,特别是考虑到k值较小。
总之,算法1和算法2之间的选择取决于n和k的具体值。对于n和k的相对小的值,算法1可以更简单和有效。对于较大的值,特别是当k显著小于n时,算法2可能更有利。
<H2>Laboratory 1 Complexity analysis Week2</H2>
1.数组随机生成,顺逆序处理。
2.10个长度均匀间隔的分类阵列算法的时间测量。
3.对小范围间隔的分类算法的时间测量。
4.特别大数量的算法时间测量,以及关于运行内存的计算机的近似极限值。
从这里明显感到新开课程的准备不足。
总之结论可以参考 此贴
总结
排序法 | 平均时间 | 最差情形 | 稳定度 | 额外空间 | 备注 |
冒泡 | O(n2) | O(n2) | 稳定 | O(1) | n小时较好 |
交换 | O(n2) | O(n2) | 不稳定 | O(1) | n小时较好 |
选择 | O(n2) | O(n2) | 不稳定 | O(1) | n小时较好 |
插入 | O(n2) | O(n2) | 稳定 | O(1) | 大部分已排序时较好 |
基数 | O(logRB) | O(logRB) | 稳定 | O(n) | B是真数(0-9) R是基数(个十百) |
Shell | O(nlogn) | O(ns)1<s<2 | 不稳定 | O(1) | s是所选分组 |
快速 | O(nlogn) | O(n2) | 不稳定 | O(nlogn) | n大时较好 |
归并 | O(nlogn) | O(nlogn) | 稳定 | O(1) | n大时较好 |
堆 | O(nlogn) | O(nlogn) | 不稳定 | O(1) | n大时较好 |
下面依次介绍lab出现的排序方法
Bubble sort
1.比较相邻的元素。如果第一个比第二个大,就交换它们两个;
2.对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
3. 针对所有的元素重复以上的步骤,除了最后一个;
4. 重复步骤1~3,直到排序完成。
public class BubbleSort {
public static void main(String[] args) {
int[] numArray={3,6,4,2,11,10,5};
//computational time complexities O(n*n)
int temp = 0;//temp
for (int j = 0; j < numArray.length - 1; j++) {
for (int i = 0; i < numArray.length-1 -j ; i++) {
if (numArray[i] > numArray[i+1]){
temp = numArray[i];
numArray[i] = numArray[i+1];
numArray[i+1] = temp;
}
}
System.out.println("第"+(j+1)+"趟"+Arrays.toString(numArray));
}
}
}
优化
public class BubbleSort {
public static void main(String[] args) {
int[] numArray={3,6,4,2,11,10,5};
int temp = 0;//temp
boolean flag = false;//Used to optimize the bubble sorting to determine whether the overexchange
for (int j = 0; j < numArray.length - 1; j++) {
for (int i = 0; i < numArray.length-1 -j ; i++) {
if (numArray[i] > numArray[i+1]){
// triangular exchange
temp = numArray[i];
numArray[i] = numArray[i+1];
numArray[i+1] = temp;
flag = true;
}
}
System.out.println("第"+(j+1)+"趟"+Arrays.toString(numArray));
//If you do not enter the triangle exchange, you can prove that the array is ordered and exit the loop directly
//If you enter the triangle exchange,
the flag as false to determine whether the next cycle enters the triangle exchange
if (flag == false){
break;
}else {
flag = false;
}
}
}
}
<H3>插入排序(Insertion Sort)</H3>
public class InsertionSort {
public static void sort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int key = arr[i];
int j = i - 1;
// Move elements of arr[0..i-1], that are greater than key,
// to one position ahead of their current position
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j]; j = j - 1;
}
arr[j + 1] = key;
}
}
}
<H3>堆排序(Heap Sort)</H3>
public class HeapSort {
public static void sort(int[] arr) {
int n = arr.length;
// Build heap (rearrange array)
for (int i = n / 2 - 1; i >= 0; i--)
heapify(arr, n, i);
// One by one extract an element from heap
for (int i = n - 1; i >= 0; i--) {
// Move current root to end
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
// call max heapify on the reduced heap
heapify(arr, i, 0);
}
}
// To heapify a subtree rooted with node i which is an index in arr[]. n is size of heap
void heapify(int arr[], int n, int i) {
int largest = i; // Initialize largest as root
int left = 2 * i + 1; // left = 2*i + 1
int right = 2 * i + 2; // right = 2*i + 2
// If left child is larger than root
if (left < n && arr[left] > arr[largest])
largest = left;
// If right child is larger than largest so far
if (right < n && arr[right] > arr[largest])
largest = right;
// If largest is not root
if (largest != i) {
int swap = arr[i];
arr[i] = arr[largest];
arr[largest] = swap;
// Recursively heapify the affected sub-tree
heapify(arr, n, largest);
}
}
}
<H3>合并排序(MergeSort)</H3>
public class MergeSort {
public static void sort(int[] arr) {
if (arr.length > 1) {
int mid = arr.length / 2;
int[] left = new int[mid];
int[] right = new int[arr.length - mid];
// Divide the array into two halves
for (int i = 0; i < mid; i++)
left[i] = arr[i];
for (int j = mid; j < arr.length; j++)
right[j - mid] = arr[j];
// Sort the two halves
sort(left);
sort(right);
// Merge the sorted halves
merge(arr, left, right);
}
}
public static void merge(int[] arr, int[] left, int[] right) {
int i = 0, j = 0, k = 0;
// Merge smaller elements first
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) {
arr[k] = left[i];
i++;
} else {
arr[k] = right[j];
j++;
}
k++;
}
// Copy remaining elements of left[] if any
while (i < left.length) {
arr[k] = left[i];
i++;
k++;
}
// Copy remaining elements of right[] if any
while (j < right.length) {
arr[k] = right[j];
j++;
k++;
}
}
}
<H3>快速排序(Quick Sort)</H3>
public class QuickSort {
public static void sort(int[] arr, int low, int high) {
if (low < high) {
// pi is partitioning index, arr[pi] is now at right place
int pi = partition(arr, low, high);
// Recursively sort elements before partition and after partition
sort(arr, low, pi - 1);
sort(arr, pi + 1, high);
}
}
// This method takes last element as pivot, places the pivot element at its correct position
// in sorted array, and places all smaller (smaller than pivot) to left of pivot and all greater elements to right of pivot
int partition(int arr[], int low, int high) {
int pivot = arr[high];
int i = (low - 1); // index of smaller element
for (int j = low; j < high; j++) {
// If current element is smaller than or equal to pivot
if (arr[j] <= pivot) {
i++;
// swap arr[i] and arr[j]
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// swap arr[i+1] and arr[high] (or pivot)
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;
return i + 1;
}
}
<H3>希尔排序 (Shell Sort)</H3>
public class ShellSort {
public static void sort(int[] arr) {
int n = arr.length;
int gap, i, j, temp;
// Start with a big gap, then reduce the gap
for (gap = n / 2; gap > 0; gap /= 2) {
// Do a gapped insertion sort for this gap size.
// The first gap elements a[0..gap-1] are already in gapped order
// keep adding one more element until the entire array is gap sorted
for (i = gap; i < n; i++) {
// add a[i] to the elements that have been gap sorted
// save a[i] in temp and make a hole at position i
temp = arr[i];
// shift earlier gap-sorted elements up until the correct location for a[i] is found
for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
arr[j] = arr[j - gap];
}
// put temp (the original a[i]) in its correct location
arr[j] = temp;
}
}
}
}
<H3>选择排序 (Selection Sort)</H3>
public class SelectionSort {
public static void sort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
int min_idx = i;
for (int j = i + 1; j < n; j++)
if (arr[j] < arr[min_idx])
min_idx = j;
// Swap the found minimum element with the first element
int temp = arr[min_idx];
arr[min_idx] = arr[i];
arr[i] = temp;
}
}
}
<H2>Lecture 5 Data Structures and Abstract Data Type</H2>
上学期Java的一些定义。
<H3>A Use Case: Phone Book </H3>
In the phone book, we have:
A set of people’ name and their phone numbers
<H3>Abstract Data Types </H3>
Primitive data type 原始数据类型
int, long, double, String, etc
Other data type, such as: 其他数据类型
List, Set, Map
are known as Abstract data types (ADTs),抽象数据类型(ADT),
i.e., a data type that consists
of a collection of values together with a set of basic operations on these
values
We can create our own abstract data type (ADT) called
Person, and keep everything together.
public class Person {
public String name;
public String contact;
}
Like other data type
Use new to create a new object, e.g.,
Person person = new Person();
Use .<attribute_name> to refer to the object’s attribute,
e.g., person.name = "Bill" will set the name of the
object person to “Bill”
public class PhoneBook {
public static void main(String... arguments) {
Person[] persons = new Person[5];
//Create an array similar to the previous one but with Person data type
persons[0] = new Person();
//For each entry we have to create a new object
persons[0].name="Alan Turing";
persons[0].contact="+86 188 1234 5678";
persons[1] = new Person();
persons[1].name="Herbert Simon";
persons[1].contact="+86 123 9876 5432";
.//and set the property values
.
persons[4]=new Person();
persons[4].name="Linus Torvalds";
persons[4].contact="+86 188 3062 4700";
String nameToSearch = "Alan Turing";
int index = linearSearch(nameToSearch, persons);
System.out.println("Name to search: " + nameToSearch);
if (index < 0) System.out.println("Contact not found!");
else System.out.println("Contact: " + persons[index].contact);
System.out.println(""); //this also needs to be changed
nameToSearch = "Edsger Dijkstra";
<H3>Object Construction and Destruction Constructor and Destructor</H3>
Constructor 构造函数
used to create an instance of the object and allocate “enough” memory to
it
每个对象都必须有一个构造函数。构造函数是在创建对象时调用的,用于初始化对象的状态。
如果没有提供构造函数,系统会自动创建一个,这被称为默认构造函数(Default Constructor)。默认构造函数通常是一个不接受任何参数的构造函数。
与其他函数类似,可以向构造函数传递数据(称为参数)以初始化对象。例如,Person person = new Person(name, contact);
这行代码创建了一个新的Person
对象,并传递了name
和contact
两个参数给其构造函数。
Destructor 析构函数
invoke automatically when the object is out of scope, and
release/free the memory back to the system
Every object must have a constructor; while the destructor is an optional
If no constructor is provided, then the system will create a constructor,
known as default constructor, automatically
A default constructor (or default value constructor) is a constructor with no
argument, e.g., Person person = new Person();
Similar to other functions, data, known as arguments, can be passed to the
constructor to initialize the object, e.g.,
Person person = new Person(name, contact);
Destructor, on the other hand, does not require any argument
· 析构函数是在对象超出其作用域时自动调用的。这意味着当一个对象不再需要时(例如,它离开了其定义的范围或者被删除),析构函数会被自动执行。
· 析构函数的主要作用是释放或归还对象所占用的内存给系统。这是为了确保资源得到有效管理,防止内存泄漏。
· 析构函数是可选的,不是每个类都必须有析构函数。当类中没有显式定义析构函数时,编译器可能会提供一个默认的析构函数(如果存在需要释放的资源,则通常建议自定义析构函数)
与构造函数不同,析构函数不需要任何参数。当对象超出其作用域或被删除时,析构函数会自动被调用,而不需要传递任何参数。析构函数的主要任务是清理和释放对象所使用的资源。
总结来说,构造函数用于初始化对象,而析构函数用于清理和释放对象所使用的资源。析构函数是自动调用的,而构造函数通常在创建对象时通过new
关键字调用。两者都是面向对象编程中重要的组成部分,用于管理对象的生命周期和资源。
public class Person {
public String name;
public String contact;
//Change all attributes’ modifiers to private
public Person(String name, String contact) {
this.name = name;
this.contact = contact;
}
public Person() {
this("", "");
}
}
public Person(String name,
String contact) {
this.name = name;
this.contact = contact;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getContact() {
return contact;
//Provide access to attributes through different methods These methods also known as “getters” and “setters” of the attributes
get与set
<H3>Data-directed design</H3>
Design directed by the choice and representation of data structures
Data requirements:
In addition to the getters and setters methods, what functions to be performed on the data
What’s the proper scope Ownership?
– who owns the data
How is it shared?
<H3>An example: Counter</H3>
Counter: A device which stores the number of times a particular event or process has occurred. Data required counter value (int)
Functions provide initialize/reset counter increment counter decrement counter return counter value.
计数器:存储特定事件或进程发生的次数的设备。
数据所需的计数器值(int)
函数提供初始化/重置计数器增量计数器递减计数器返回计数器值。public class Counter {
private int value;
/**
* Initialize/reset the
counter
*/
public void reset() {}
/**
* Increment counter
*/
public void increment() {}
/**
* Decrement counter
*/
public void decrement() {}
/**
* Get counter value
*
* @return value of the
counter
*/
public int getValue() {}
}
Can we use a constructor here to initialize the counter and remove the reset method?
Both options can be used to initialize the counter. It depends on personal specific requirements and design preferences.
这两个选项都可用于初始化计数器。这取决于具体要求和设计偏好。
public Counter() {
value = 0;
}
or
public Counter() {
initialize();
}
<H3>value = 0; </H3>
Using Constructor to Initialize Counter: If you choose to use a constructor to initialize the counter, you can remove the reset() method. In this case, you would modify the Counter class as follows:
使用构造函数初始化计数器:如果选择使用构造函数初始化计数器,可以删除 reset() 方法。在这种情况下,您可以按如下方式修改Counter类:
package counter1;
public class Counter1 {
private int value;
// Initialize/reset the counter
public void initialize() {
value = 0;
}
// Get counter value
public int getValue() {
return value;
}
// Increment counter
public void increment() {
if (value < Integer.MAX_VALUE) {
value++;
} else {
System.out.println("Counter overflow: Increment ignored!");
}
}
// Decrement counter
public void decrement() {
if (value > Integer.MIN_VALUE) {
value--;
} else {
System.out.println("Counter underflow: Decrement ignored!");
}
}
// Constructor to initialize the counter
public static void main(String[] args) {
// Create a Counter object
Counter1 counter = new Counter1();
// Get the initial counter value
int value = counter.getValue();
System.out.println("Initial Counter Value: " + value); // Output: Initial Counter Value: 0
// Increment the counter
counter.increment();
// Get the updated counter value
value = counter.getValue();
System.out.println("Updated Counter Value: " + value); // Output: Updated Counter Value: 1
// Decrement the counter
counter.decrement();
// Get the final counter value
value = counter.getValue();
System.out.println("Final Counter Value: " + value); // Output: Final Counter Value: 0
// Reinitialize the counter
counter.initialize();
// Get the reinitialized counter value
value = counter.getValue();
System.out.println("Reinitialized Counter Value: " + value); // Output: Reinitialized Counter Value: 0
}
}
数据由 value = X;来决定
In this version, whenever a Counter
object is created using the constructor (Counter counter = new Counter();
), the counter will be automatically initialized to 0. There is no need to call a separate initialization method.
在这个版本中,每当使用构造函数( Counter counter = new Counter();
)创建 Counter
对象时,计数器将自动初始化为0。不需要调用单独的初始化方法。
Using Initialize Method: Alternatively, if you prefer to keep the reset()
method, you can modify the constructor to call the reset()
method instead of directly initializing the value. You can do this by adding an initialize()
method that calls reset()
, and then use that method in the constructor. Here's an example:
使用初始化方法:或者,如果更喜欢保留 reset()
方法,则可以修改构造函数以调用 reset()
方法,而不是直接初始化值。您可以通过添加一个调用 reset()
的 initialize()
方法来实现这一点,然后在构造函数中使用该方法。下面是一个示例:
publicclass
Counter
{
private
int
value;
// Initialize/reset the counter
public
void
initialize()
{
reset();
}
// Get counter value
public
int
getValue()
{
return
value;
}
// Increment counter
public
void
increment()
{
if
(value < Integer.MAX_VALUE) {
value++;
}
else{
System.out.println(
"Counter overflow: Increment ignored!");
}
}
// Decrement counter
public
void
decrement()
{
if
(value > Integer.MIN_VALUE) {
value--;
}
else{
System.out.println(
"Counter underflow: Decrement ignored!");
}
}
// Constructor to initialize the counter
public
Counter()
{
initialize();
}
public static void main(String[] args) {
// Create a Counter object
Counter counter = new Counter();
// Get the initial counter value
int value = counter.getValue();
System.out.println("Initial Counter Value: " + value); // Output: Initial Counter Value: 0
// Increment the counter
counter.increment();
// Get the updated counter value
value = counter.getValue();
System.out.println("Updated Counter Value: " + value); // Output: Updated Counter Value: 1
// Decrement the counter
counter.decrement();
// Get the final counter value
value = counter.getValue();
System.out.println("Final Counter Value: " + value); // Output: Final Counter Value: 0
// Reinitialize the counter
counter.initialize();
// Get the reinitialized counter value
value = counter.getValue();
System.out.println("Reinitialized Counter Value: " + value); // Output: Reinitialized Counter Value: 0
}
}
数据由 value = X;来决定
意义不明
<H3>An example: Complex number </H3>
package complex;
public class Complex {
private double real;
private double imag;
public Complex(double real, double imag) {
this.real = real;
this.imag = imag;
}
public Complex() {
this(0.0, 0.0);
}
public void add(Complex complex) {
this.real += complex.real;
this.imag += complex.imag;
}
public void multiply(Complex complex) {
double newReal = this.real * complex.real - this.imag * complex.imag;
double newImag = this.real * complex.imag + this.imag * complex.real;
this.real = newReal;
this.imag = newImag;
}
// Getters and setters for real and imag
public double getReal() {
return real;
}
public void setReal(double real) {
this.real = real;
}
public double getImag() {
return imag;
}
public void setImag(double imag) {
this.imag = imag;
}
public static void main(String[] args) {
Complex complex1 = new Complex(1, 2);
Complex complex2 = new Complex(1, 2);
System.out.println("complex1.real=" + complex1.getReal());
System.out.println("complex1.imag=" + complex1.getImag());
System.out.println(complex1.equals(complex2));
System.out.println(complex1==complex2);
}
}
Output:
complex1.real=1.0
complex1.imag=2.0
false
false
<H4>Why
?
</H4>
<H5>complex1.equals(complex2)</H5>
object类的equals方法默认比较的两个对象的地址,所以为了正确地比较两个Complex
对象的值,可以基于real
和imag
的值覆盖Complex
类中的equals
方法,如下所示:
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
//特殊情况,同一对象则直接true
if (obj == null || getClass() != obj.getClass()) {
return false;
}
//特殊情况,obj的有null或者obj的类不是同一类型
Complex other = (Complex) obj;
//将传入的对象obj强制类型转换为Complex类型,并赋值给other变量
return Double.compare(real, other.real) == 0 &&
Double.compare(imag, other.imag) == 0;
//使用Double.compare方法比较当前对象的实部和虚部与other对象的实部和虚部是否相等。如果都相等,则返回true;否则返回false。
}
这个时候,输出的就是true了。
<H5>complex1==complex2</H5>
尽管complex1
和complex2
对于real
和imag
具有相同的值,但它们是使用new
关键字实例化为单独的对象的,因此它们将具有不同的内存地址。因此会显示false
这个作业的用意是
:
1.When creating an ADT, we have to tell the computer how to compare two objects
2.Different programming languages have different approaches.
In C++, We can overload the operators, by providing the operators with a special meaning for a data type without changing its original meaning.
In Java
Every class (except primitives) is a subclass of the Object class
Allows every common methods and attributes (properties) to be available upon its creation, e.g.:
clone() – create an exact copy of the object
hashCode() – an integer representing, also known as hash
code, of the object
equals() – check whether two objects has the same hash
code
toString() – a string representation of the object
<H2>Lecture 6-7 Data Structures and Abstract Data Type Data Abstraction</H2>
<H3>Data Abstraction</H3>
根据数据结构的选择和表示方式进行设计
数据要求:除了
get
和
set
方法外,应对数据执行什么功能?适当的范围所有权是什么?
–
谁拥有数据如何共享?
<H3>Data requirements</H3>
In addition to the getters and setters methods, what functions to be performed on the data
What’s the proper scope Ownership?
– who owns the data
How is it shared?
<H3>Concept of an Object<H3>
Members
Attributes
Data members
Data type definitions
Member functions
Basic access controls
Public
Protected
Private
Information hiding
Data encapsulation
Abstract data types (ADTs)
<H3>Levels of Abstraction<H3>
<H3>Data Abstraction Example: Indexed Set</H3>
编写的每个部分都要补全,并演示其使用。
Constants
Variables
Methods
Constructors
Getters/setters
由于
有两个
addItem
方法的声明,我们只需要一个。该方法应该检查数组是否已满,然后添加元素并返回新元素的索引。
示例使用
现在我们可以演示如何使用这个
IndexSet
类:
package indexsetdemo;
public class IndexSetDemo {
private static final int DEFAULT_MAX_SIZE = 20;
private static final int INVALID_INDEX = -1;
private int maxSize;
private int size; //
存储当前集合中的元素数量
private double[] items; //
存储元素的数组
//
构造函数,初始化
maxSize
和
items
数组
public IndexSetDemo(int maxSize) {
this.maxSize = maxSize;
this.items = new double[maxSize];
this.size = 0; //
初始化时集合为空
}
//
默认构造函数,使用
DEFAULT_MAX_SIZE
作为
maxSize
public IndexSetDemo() {
this(DEFAULT_MAX_SIZE);
}
/**
*
添加一个元素到集合中
*
* @param item
要添加的元素
* @return
元素在集合中的索引
*/
public int addItem(double item) {
if (size < maxSize) {
items[size] = item;
return size++; //
返回当前索引,并递增
size
} else {
System.err.println("
集合已满!无法添加更多元素!
");
return INVALID_INDEX;
}
}
/**
*
检索集合中指定索引的元素
*
* @param index
元素的索引
* @return
元素的值
* @throws IllegalArgumentException
如果索引超出范围
*/
public double retrieve(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("
索引超出范围
: " + index);
}
return items[index];
}
/**
*
获取集合的大小
*
* @return
集合的大小
*/
public int getSize() {
return size;
}
public static void main(String[] args) {
IndexSetDemo set = new IndexSetDemo(); //
创建集合对象
int index1 = set.addItem(3.14); //
添加元素并获取索引
int index2 = set.addItem(2.71); //
添加元素并获取索引
double item1 = set.retrieve(index1); //
检索元素
double item2 = set.retrieve(index2); //
检索元素
System.out.println("
索引
" + index1 + "
的元素
: " + item1);
System.out.println("
索引
" + index2 + "
的元素
: " + item2);
System.out.println("
集合的大小
: " + set.getSize());
int index3 = set.addItem(1.618); //
尝试添加新元素并获取索引
System.out.println("
新元素的索引
: " + index3);
//
尝试检索超出范围的索引,将抛出异常
try {
double item3 = set.retrieve(5);
System.out.println("
索引
5
的元素
: " + item3);
} catch (IllegalArgumentException e) {
System.out.println("
错误
: " + e.getMessage());
}
}
}
这个示例展示了如何创建一个
IndexSet
对象,添加元素,检索元素,以及获取集合的大小。它还演示了当尝试添加超过最大容量的元素时,
addItem
方法返回无效索引的情况,以及当尝试检索超出范围的索引时抛出
IndexOutOfBoundsException
异常的情况。
<H3>ComplexSet </H3>
现在我们需要把
IndexSet
改为
ComplexSet
所需数据:
数据阵列(
Complex
)
功能提供:
向集合中添加一个复数
查找该集中是否存在复数的副本
获取集合的大小。
约束条件:
没有重复的项目
可变尺寸
Constants
private static final int
DEFAULT_INITIAL_CAPACITY = 2;
private static final double
GROWTH_FACTOR = 1.5;
private static final int
INVALID_INDEX = -1;
Variables
private int maxSize;
private int size;
private Complex[] items;
Constructors
public ComplexSet(int initialSize) {
this.items = new Complex[initialSize];
for (int i = 0; i < initialSize; i++) this.items[i] = null;
size = 0;
maxSize = initialSize;
}
public ComplexSet() {
this(DEFAULT_INITIAL_CAPACITY);
}
Getters/Setters
public int getSize() {
return size;
}
Methods
/**
* retrieve an indexed element in the set
*
* @param index of the element
* @return value of the element
*/
public Complex retrieve(int index) {
if (index < 0 || index >= size) throw new IllegalArgumentException("
Index out of range: " + index);
return items[index];
}
/**
* check whether an identical copy of item is already in the set
*
* @param item to be checked
* @return index of the item if it is already in the set;
*
ItemSet.INVALID_INDEX otherwise
*/
public int isInSet(Complex item) {
if (null == item) return INVALID_INDEX;
for (int i = 0; i < size; i++) {
if (items[i].equals(item)) return i;
}
return INVALID_INDEX;
}
private void growthSet() {
int newMaxSize = (int) (GROWTH_FACTOR * maxSize);
System.out.println(getClass().getSimpleName() + ": new set size=" +
newMaxSize);
// create an array with new max size
Complex[] newItems = new Complex[newMaxSize];
// copy the items to the new array
for (int i = 0; i < size; i++) {
newItems[i] = items[i];
}
// initialize the rest of the array to null
for (int i = size; i < newMaxSize; i++) {
newItems[i] = null;
}
// replace the old array with the new one
// and update the new max size
items = newItems;
maxSize = newMaxSize;
}
/**
* add an item to the set
*
* @param item to be added
* @return index of item in the set
*/
public int addItem(Complex item) {
if (null == item) return INVALID_INDEX;
// check whether the item is in the set
int index = isInSet(item);
if (index >= 0) return index;
// check whether there is any free space in the set
// and increase the size of the set if not
if (size == maxSize) growthSet();
items[size] = item;
return size++;
}
<H3>Summary </H3>
以上两个例子展示了如何使用
ADT
来存储信息,并从
data
那里提取实现细节。
两个例子的共性
将项目存储在一个数组中。
同一组方法,接口不变,
getSize(), retrieve(), addItem()
<H2>Lecture 8: Recursion</H2>
<H3>Recursion introduction</H3>
A recursive function is a function that calls itself directly or
indirectly
有助于将较大的复杂问题分解成较小的问题
Recursion requires:
Stopping cases (a.k.a. exit conditions, base case) –
要有一个退出条件。简单的情况,有简单的解决方案。
Recursive step:
The problem can be reproduced to identical but less complex subproblem(s).
▶ Reduce the problem to identical but smaller problem(s).
分解问题为小问题小步骤
Eventuality:
The problem can eventually be reduced to the
stopping cases through the recursive step, i.e., a check for
termination is need.
<H3>二分法(Bisection Method)</H3>
如何使用二分法(Bisection Method)来寻找一个连续函数在某个区间内的根的。二分法是一种数值方法,适用于在连续函数上找到满足 f(x) = 0 的 x 值。
if (Math.abs(upper - lower) < threshold) return (upper + lower) / 2;
如果lower和upper之间的差的绝对值小于某个阈值threshold,则函数返回这两个值的平均值,作为近似根。
这是二分法的终止条件。当区间足够小时,我们可以认为区间的中点就是函数的根。
double midpt = (upper + lower) / 2;
if (differentSign(f(midpt), f(lower)))
else if (differentSign(f(midpt), f(upper)))
低-中、中-高二分法
根据函数符号的变化是二分法的关键。
<H4>moveDisks 方法</H4>
1.这是一个递归方法,用于移动nDisk
个盘子从from
柱子到to
柱子,使用intermediate
柱子作为辅助。
2.如果nDisk
小于2(即只有一个或没有盘子),则直接将其从from
移动到to
。
3.否则,首先移动nDisk - 1
个盘子从from
到intermediate
,使用to
作为辅助柱子(注意这里的角色转换)。
4.然后移动最大的盘子(即第nDisk
个盘子)从from
到to
。
5.最后,将nDisk - 1
个盘子从intermediate
移动到to
,使用from
作为辅助柱子。
<H3>Example Hanoi Tower</H3>
汉诺塔(Hanoi Tower)是一个经典的递归问题。它涉及到三个柱子和一系列不同大小的盘子,所有的盘子都有一个孔可以放在柱子上。开始时,所有的盘子都按照大小顺序堆叠在第一个柱子上,大的盘子在下面,小的盘子在上面。目标是将这些盘子移动到第三个柱子上,同时遵守以下规则:
1.一次只能移动一个盘子。
2.大的盘子不能放在小的盘子上面。
要解决这个问题,需要使用递归的方法。
public class Hanoi {
private static String[] TOWERS = new String[] { "A", "B", "C" };
public void moveDisks(int nDisk, String from, String to, String intermediate) {
if (nDisk < 2) { // stopping case
System.out.printf("Move Disk \"%s\" from Tower %s to Tower %s\n", nDisk, from, to);
} else { // recursion step
moveDisks(nDisk - 1, from, intermediate, to);
System.out.printf("Move Disk \"%s\" from Tower %s to Tower %s\n", nDisk, from, to);
moveDisks(nDisk - 1, intermediate, to, from);
}
}
// initial call to recursive function
public void run(int nDisks) {
moveDisks(nDisks, TOWERS[0], TOWERS[1], TOWERS[2]);
}
public static void main(String[] arguments) {
Hanoi hanoi = new Hanoi();
hanoi.run(5);
}
}
<H4>变量和常量</H4>
1.TOWERS 是一个字符串数组,表示三个柱子的名字:A、B、C。
2.nDisk 表示当前要移动的盘子的数量。
3.from, to, 和 intermediate 分别表示源柱子、目标柱子和中间柱子。
<H4>run 方法</H4>
1.这是moveDisks
方法的初始调用,使用TOWERS
数组中的值作为三个柱子的名字。
<H4>main 方法</H4>
创建一个Hanoi
对象并调用run
方法,开始移动5个盘子。
<H4>The principle of the Hanot Tower</H4>
汉诺塔的原理基于分治策略(Divide and conquer algorithms)。问题被分解为更小、更简单的子问题,然后递归地解决这些子问题。对于n
个盘子的汉诺塔问题,我们将其分解为:
1.将n-1
个盘子从起始柱子移动到辅助柱子。
2.将最大的盘子从起始柱子移动到目标柱子。
3.将n-1
个盘子从辅助柱子移动到目标柱子。
每次递归调用都减少了问题的规模,直到问题变得足够简单,可以直接解决(即移动一个或没有盘子)。这种分治策略使得汉诺塔问题可以通过递归有效地解决。
总的来说,代码正确地实现了汉诺塔问题的递归解决方案,并清晰地展示了其原理和步骤。
<H3>Problems faced with using the recursion </H3>
Common problems
Space complexity Recursive solutions often require additional memory for
each recursive call, as the stack grows with each function
invocation, which can lead to high space complexity, particular
for problems with large input sizes or deep recursion.
递归函数在调用自身时,每次调用都会在内存中创建一个新的栈帧来保存该次调用的局部变量和返回地址。如果递归深度很大,那么栈的大小也会迅速增长,从而消耗大量的内存。递归解决方案经常需要为每次递归调用分配额外的内存,因为每次函数调用都会使栈增长。
这可能导致高空间复杂度
Function call overhead Each recursive function call incurs overhead in terms
of time and memory due to parameter passing, activation
records, and return results after execution. This overhead will
become more significant and create impact to the performance
when a large number of recursive call are made.
每次递归函数调用都会因为参数传递、激活记录和返回结果而产生时间和内存上的占用。当递归调用次数很多时,这些累积效果会变得非常显著。
Stack Overflow If the recursion depth is too high, it can lead to a stack
overflow, causing the program to crash.
递归过高会导致栈溢出,使程序崩溃。
每个递归调用会在栈上分配空间。如果递归的深度超过了栈的最大容量,就会发生栈溢出错误,导致程序异常终止。
Redundant computation Recursive functions may perform redundant
computations by repeatedly solving the same subproblems,
leading to unnecessary work and increased time complexity.
可能会通过反复解决相同的子问题计算冗余,导致不必要的工作和增加的时间复杂度。
Difficulty in understanding and debugging As discussed in the Towers of
Hanoi example, it can be difficult to follow the flow of execution
and identify logical errors or edge cases, making debugging
more difficult.
递归算法的执行流程通常比较复杂,尤其是在解决复杂问题时。跟踪和理解递归调用的顺序和逻辑可能会很困难,尤其是在出现错误或异常情况时。
Non-optimal time complexity Not all recursion can lead to optimal time
complexity! Recursive algorithms that do not follow a
divide-and-conquer or memorization approach may result in
exponential or higher polynomial time complexity, making them
inefficient for large problem sizes
<H2>Laboratory 2 Recursive</H2>
用循环语句和递归分别检验回文。
要点:
1.循环与递归的使用
2.标点和空格不计,只看字母和数字(不区分大小写)。
以下是我的一个方案:
package xjtlu.cpt108.recursion.palindrome;
import java.util.regex.Pattern;
public class Palindrome {
int n = 0 ;
//Import n to detect the number of recursions. An empty string recurses only once, so n must be 0 before recursion
public boolean verifyRecursive(String str) {
if (str == null || str.isEmpty() && n == 0) {
return false;
}
n=n+1;//The count of recursion times
String cleanedStr = cleanString(str);
if (cleanedStr.length() <= 1) {
n=0;//Recursive return boolean returns n to 0 for later counting
return true;
}
char first = cleanedStr.charAt(0);
char last = cleanedStr.charAt(cleanedStr.length() - 1);
if (first != last) {
n=0;//Recursive return boolean returns n to 0 for later counting
return false;
}
return verifyRecursive(cleanedStr.substring(1, cleanedStr.length() - 1));
}
public boolean verifyIterative(String str) {
if (str == null || str.isEmpty()) {
return false;
}
String cleanedStr = cleanString(str);
int left = 0;
int right = cleanedStr.length() - 1;
while (left < right) {
if (cleanedStr.charAt(left) != cleanedStr.charAt(right)) {
return false;
}
left++;
right--;
}
return true;
}
private String cleanString(String str) {
str = str.toLowerCase();
StringBuilder cleaned = new StringBuilder();
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (Character.isLetterOrDigit(c)) {
cleaned.append(c);
}
}
return cleaned.toString();
}
// Main method, used for testing
public static void main(String[] args) {
Palindrome palindrome = new Palindrome();
System.out.println(palindrome.verifyRecursive("Step on no pets!")); // true
System.out.println(palindrome.verifyIterative("Step on no pets!")); // true
System.out.println(palindrome.verifyRecursive("Never odd or even")); // true
System.out.println(palindrome.verifyIterative("Never odd or even")); // true
System.out.println(palindrome.verifyRecursive("race a car")); // false
System.out.println(palindrome.verifyIterative("race a car")); // false
System.out.println(palindrome.verifyRecursive("")); // false
System.out.println(palindrome.verifyIterative("")); // false
System.out.println(palindrome.verifyRecursive(",")); // true
System.out.println(palindrome.verifyIterative(",")); // true
}
}
注意这次实验不仅仅有正则表达式还进行了split方法的教学,以及另一种回文思路,就是进行字符串的tostring处理后,再用将字符串颠倒处理,与原字符串比较。
<H3>Split()</H3>
对字符串以给定的字符进行分隔,得到字符串数组。
split(String regex,int limit)/split(String regex)
<H4>2.1 split(String regex)</H4>
<H5>1.单个分隔符</H5>
public class Test {
public static void main(String[] args) {
String str="2018,text,今天";
//单个分隔符用引号括起来即可
String[] data = str.split(",");
for(int i=0;i< data.length;i++){
System.out.println(data[i]);
}
}
}
Output:
2018
text
今天
Tips: 如果分隔符本身就是"|",那么就需要使用转义字符"\"让其产生效果,否则结果相反。
public class Test {
public static void main(String[] args) {
String str="a|bc|8";
//java中\\表示一个普通\,\+特殊字符表示字符本身
String[] data = str.split("|");
for(int i=0;i< data.length;i++){
System.out.println(data[i]);
}
}
}
Output:
a
|
b
c
|
8
<H5>2.正则表达式的应用split</H5>
3.
public class Test {
public static void main(String[] args) {
String str="2018年11月18日abcd85gg688";
//正则表达式中\d+表示一个或多个数字,java中\\表示一个普通\
String[] data = str.split("\\d+");
for(int i=0;i< data.length;i++){
System.out.println(data[i]);
}
}
}
Output:
年
月
日abcd
gg
(这个空格揭示了有数字就换行的分割原理)
<H5>3.多个分割符</H5>
public class Test {
public static void main(String[] args) {
String str="2021年11月18日;英语,数学,语文;";
//多个分隔符用引号括起来,并且用“|”进行分割 ,&;
String[] data = str.split(",|;");
for(int i=0;i< data.length;i++){
System.out.println(data[i]);
}
}
}
Output:
2021年11月18日
英语
数学
语文
· 字符串开头有分隔符:开头产生一个空字符串,其余正常。
· 分隔符相互紧挨着:每两个分隔符产生一个空字符串,若有三个分隔符则会有2各空字符,以此类推。
· 字符串最尾部有分割符:末尾产生一个空字符串,其余正常
<H4>2.2 split(String regex, int limit)</H4>
1.如果 limit > 0,(从左到右)最多分割 n - 1 次,数组的长度将不会大于 n,结尾的空字符串不会丢弃。
2.如果 limit < 0,匹配到多少次就分割多少次,而且数组可以是任何长度。结尾的空字符串不会丢弃。
3.如果 limit = 0,匹配到多少次就分割多少次,数组可以是任何长度,并且结尾空字符串将被丢弃。
<H3>Laboratory 2: Extra Part Fibonacci</H3>
public class Fibonacci {
public static int fibonacci(int n) {
if (n <= 1) {
return n;
} else {
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
public static void main(String[] args) {
Scanner n = new Scanner(System.in);
String next = n.nextLine();
int num=Integer.parseInt(next);
System.out.print(fibonacci(num)+"\n");
}
}
<H2>Lecture 9-12 Comparison Based Sorting </H2>
这里是接着Sort实验的后续具体讨论部分。
<H3>L9-10 Sorting Basics</H3>
A process of rearranging data elements based on some relationship between them.
Sort定义:元素之间按某种关系重新排列的过程
好处:
降低了检索对应元素的复杂度;
实现有效的数据存储、搜索和检索。
集合中元素的排序遵循以下关系。
An ordering relation for any elements in a set has the following properties (Knuth, 1988):
Law of Trichotomy 三分法
指的是对于任意两个实数a和b必有</=/>三种情况
Law of Transitivity 传递性
如果(a,b)∈X (b,c)∈X,则(a,b,c)∈X
Sorting
对于字符串,我们仍然可以排序。我们可以排字符串的长度,我们也可以排字符串首字母顺序。
逆序inversion
逆序通常指的是数组或链表中元素的位置与其应有的顺序不一致。例如,在一个应该按升序排列的数组中,如果较大的数出现在较小的数之前,那么这两个数就构成了一个逆序。
排序算法的目标就是消除这些逆序,使数组或链表中的元素按照指定的顺序(升序或降序)排列。通过比较和交换元素,排序算法能够逐步减少逆序的数量,直到有序。
Sorting Algorithms
Comparison Based
Selection Sort
Insertion Sort
Bubble Sort
Quick Sort
Merge Sort
Heap Sort
Non-comparison Based
Counting Sort
Radix Sort
<H3>Selection sort</H3>
Pseudocode
For each elements in the array
Set the first unsorted element as the minimum
Find the smallest element in the unsorted portion of the array
If smallest < minimum
swap(minimum, smallest)
选择第一个未排序元素,然后将未排序的最小元素和第一个未排序元素进行互换,互换后第一个元素的位置就算已排序元素。接着继续对未排序元素进行排序直到排序完位置。
时间复杂度:
最好情况:O(n^2)
平均情况:O(n^2)
最坏情况:O(n^2)
空间复杂度:O(1)(不需要额外的存储空间)
第一轮,最值的获得,第一位的值一个一个对每个位置进行比较,只要大的话就swap,因为无法得知哪个是最值所以要进行遍历。用了一次。一共用了n-1次。
第二轮,一共用了n-2次。
...
第n-1轮,一共用了1次。
综上次数为:nX(n-1)/2
所以,是n^2级的时间复杂度。
Advantages
Simple and easy to implement
Works well with small dataset
It is an in-place algorithm, as it does not require extra space
简单;小数据集;不需要额外空间
Disadvantages
Complexity of O(N^2) in the worse-case
Does not work well on large dataset
Does not preserve relative order of elements with equal value, which
means it is not stable
复杂性最差情况O(N^2)
不适应大数据集
没有考虑等值元素的顺序:不稳定----
实际上,等值元素最好保持着原本的顺序。然而,选择排序并不具备这种稳定性。在选择排序的过程中,如果有多个元素具有相同的值,它们的相对顺序可能会发生变化。因为元素不止有值,可能还有其他属性。
<H3>Insertion sort</H3>
Pseudocode
Mark first element as sorted
For each element key in the unsorted portion of the array
Compare key with the element to its left
If the current element is smaller
swap them
else
continue with next element
选择未排序第一位的元素,与前一位已排序的做比较,比前一位小就交换位置。直到无法交换位置,则排序了一位。再次重复排序,直到排序完毕为止。
时间复杂度:
最好情况:O(n)(当输入数组已经排序时)
平均情况:O(n^2)
最坏情况:O(n^2)
空间复杂度:O(1)(不需要额外的存储空间)
如果是最坏情况,也就是每轮都要最大次数的话。
第一轮,最值的获得,从第一位的值跟前一位比较,只要第一位的值大的话就swap,然后继续将原第一位的值和前一位比较,此后正好都是最坏情况,每一次都是后一位小。一共用了n-1次。
第二轮,一共用了n-2次。
...
第n-1轮,一共用了1次。
综上次数为:nX(n-1)/2 O(n^2)
所以,是n^2级的时间复杂度。
最好情况就是正好全是第一次判断就复合O(n)
Characteristics of Insertion Sort
Simple and easy to implement
Efficient for small dataset
Adaptive in nature, i.e., it is appropriate for datasets that are already
partially sorted
It is an in-place algorithm, as it does not require extra space
It preserves the relative order of elements with equal value, which means
it is stable
It is stable meaning that it preserve the relative order of elements with
equal value
简单易行,对小数据集有效,使用原数据集,空间复杂度不需要额外,保持了等值元素的相对顺序(稳定)。
<H3>Bubble sort</H3>
从左到右,前一位和后一位比较,前一位大则swap,进行n-1次算上循环一共是n次操作。有n个元素就重复n-1次。
冒泡排序
时间复杂度:
最好情况:O(n)(当输入数组已经排序时)
平均情况:O(n^2)
最坏情况:O(n^2)
空间复杂度:O(1)(不需要额外的存储空间)
Advantages
Easy to understand and implement
Require no additional space
It is an in-place algorithm, as it does not require extra space
It preserves the relative order of elements with equal value, which means
it is stable
易于理解和实现,空间复杂度不需要额外,保持等值元素相对顺序(稳定)
Disadvantages
Complexity of O(N^2) which makes it very slow for large dataset
很明显的重复累赘算法,处理大数据集缺陷。
所以很明显我们可以进行改进。
Bubble sort: An Improved Version
加一个标识符,只要内层没发生交换,就可以提前结束。
改进前
For subarray_size =0 to N-2
For index=0 to subarray_size -2
if index-th element < (index+1)-th element
swap the two elements
改进后
For subarray_size =0 to N-2
isSorted=True
For index=0 to subarray_size -2
if index-th element < (index+1)-th element
swap the two elements
isSorted=False
If (isSorted) break the loop
JAVA
public class ImprovedBubbleSort {
public static void improvedBubbleSort(int[] array) {
int n = array.length;
boolean swapped;
for (int i = 0; i < n - 1; i++) {
swapped = false;
for (int j = 0; j < n - i - 1; j++) {
if (array[j] > array[j + 1]) {
// 交换元素
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
swapped = true;
}
}
// 如果内层循环始终没有发生交换,说明数组已经有序,可以提前终止
if (!swapped) {
break;
}
}
}
public static void main(String[] args) {
int[] array = {64, 34, 25, 12, 22, 11, 90};
System.out.println("原始数组:");
for (int num : array) {
System.out.print(num + " ");
}
System.out.println();
improvedBubbleSort(array);
System.out.println("排序后的数组:");
for (int num : array) {
System.out.print(num + " ");
}
}
}
Summary
算法的复杂度
Sort L9L10 | Best | Worse |
Selection sort | O(n^2) | O(n^2) |
Insertion sort | O(n^2) | O(n) |
Bubble sort | O(n^2) | O(n^2) |
Improved Bubble sort | O(n^2) | O(n) |
<H3>L11 Sortings</H3>
就是把问题分解为小问题去解决。
<H3>Merge sort</H3>
Pseudocode
Given a sequence with N elements
Divide the sequence into two smaller subsequences
Sort each smaller subsequence recursively
Merge the two sorted subsequences to produce one sorted sequence
归并排序采用分治法的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
分割(Divide step):
把待排序的数组递归地分割成两个更小的子数组,直到每个子数组只包含一个元素。这时候,每个子数组自然就是有序的,因为只有一个元素嘛。
每分割一次算一步。也就是除以2,直到只有单个元素。每个元素都被分割过所以O(1)Xn
O(log2n)这也被写作O(logn)
如何分割?mid = (left + right)/2 根据左右确定中间的位置
递归排序(Conquer step):
对每个子数组进行排序。由于子数组已经很小了(只有一个元素),所以这一步其实没什么可做的,但为了保持递归的一致性,我们还是这么写。
这一步理解为分割的步骤里面还是保留了原数组的顺序。
2^k X (N /2^k) = N 所以是O(n) 因为要N /2^k变为单个,所以N = 2^k k=logN
合并(Combine step):
把排好序的子数组合并成一个新的有序数组。这一步是关键,也是归并排序名字中“归并”的来源。合并时,我们比较两个子数组的第一个元素,把较小的那个放入新的数组,然后移动指针继续比较,直到一个子数组的所有元素都放入新数组。最后,把另一个子数组剩余的元素(如果有的话)依次放入新数组。
这个部分很有意思,
比如有排序过的子数组ABC和DEF。
比较发现A<D,则Input 第一位A;
发现B>D,则第二位D;
发现B<E,则第三位B;
发现C<E,则第四位C;
然后DEF顺序本来就排好了,所以EF是第五第六位。
这样一个步骤算的空间复杂度,2个元素为n/2个的子数组一轮最多有可能操作n次。也就是说有一共n个元素就是O(n)。一共要k轮,与分割相同。
T(N) = 2T(N/2) + N
= 2[2T(N/4) + N/2]+ N
= 4T(N/4) + 2N =
4[2T(N/8) + N/4]+ 2N
= 8T(N/8) + 3N
= · · ·
= 2^kT(N/2^k) + kN
= N · T(1) + kN
= N + N log N
= O(N log N)
时间复杂度:
最好情况:O(n log n)
平均情况:O(n log n)
最坏情况:O(n log n)
空间复杂度:O(n)(需要额外的存储空间来合并子数组)
Advantages
Merge sort can easily parallelized to take advantage on multiple
processors or threads
Can be used in external sorting, where data to be sorted is too large to fit
into memory
Guaranteed worst-case performance (O(n log n)) which means it
performs well even on large datasets
Good to sort large datasets
Can be adapted to handle different input distributions, such
as partially sorted
并行多线程
大数据集表现良好
不同分段输入也行
Drawbacks
Space complexity
It requires additional memory to store the sorted data during
the sorting process
High overhead for small datasets
额外空间占用
对于小数据集的高占用
<H3>Quick sort</H3>
Quick Sort(快速排序)
时间复杂度:
最好情况:O(n log n)
平均情况:O(n log n)
最坏情况:O(n^2)(当输入数组已经排序或逆序时)
空间复杂度:O(log n)(递归调用栈)或 O(n)(使用非递归实现)
Pseudocode
Choose an element from the array as pivot
Reorder the array so that
all elements with values less than the pivot come before it,
while all elements with values greater than the pivot come after it
Recursively apply the above steps to the sub-array of elements
with smaller and larger values
Assumption:
A random pivot
No cutoff for small arrays
Running time:
Pivot selection: constant time, i.e., O(1)
Partitioning: linear time, i.e., O(N)
Running time of the two recursive calls:
T(1) = 1
T(N) = T(i) + T(N − i − 1) + cN
where c is a constant and i is the number
of elements in the left partition
For simplicity, we assume N is a power of 2, i.e.,
N = 2 k or k = log N, where k is a constant.
Average/Best case
i.e., when the partition is balanced, we have
T(i) ≈ T(N − i − 1) = T( N/2 )
∴ T(N) = 2T( N/2 ) + cN = 2 [2T( N/4 ) + c N/2] + cN = 4T( N/4 ) + 2cN = · · · = 2^kT( N/2^k ) + kcN = N · T(1) + kcN = N + c · (N log N) = O(N log N)
教学材料选的基准元素(pivot)是最后一位,
Divide
将比基准元素小的元素筛选出来的子数组放在左边;
比基准元素大的元素筛选出来的子数组放在右边。
在子数组中,运用同一个方法,
将比基准元素小的元素筛选出来的子数组放在左边;
比基准元素大的元素筛选出来的子数组放在右边。
直到每个子数组只有一个元素。
根据divide顺序进行排序。
每次进行divide,都要拿最后一位和n-1个元素比较大小
假设2^k=n,那么显而易见的divide用了kn次也就是nlogn
Merge
merge 步骤是指将两个已排序的子数组合并成一个有序数组的过程,这个过程确实需要 O(n) 的时间复杂度,其中 n 是子数组的元素总数。由于归并排序的递归结构,每一层递归都会涉及到 merge 操作,而递归的深度是 O(log n),因此归并排序的总时间复杂度是 O(n log n)。
总结来说,Quick Sort 基本不需要merge 步骤,快速排序的时间复杂度主要来自于其划分和递归排序的过程。
综上所述,为O(nlogn).
考虑最坏情况
在最坏情况下,递归树的深度将是n,因为每次划分操作都只会减少一个元素。在每一层递归中,都需要遍历整个数组来进行划分,这一步的时间复杂度是O(n)。因此,最坏情况下的总时间复杂度是O(n) * O(n) = O(n^2)。
Worst-case
i.e., when the partition is unbalanced
∴ T(N) = T(N − 1) + cN
= [T(N − 2) + c(N − 1)] + cN
= T(N − 2) + c [N + (N − 1)]
= · · ·
= T(1) + cΣN
i=2 i
= T(1) + c N(N 2 + 1) − 1
= O(N 2 )
Advantages
It is efficient on large datasets
It has a low overhead, as it only requires a small amount of memory to
function
大数据集有效
只需要少量内存
Drawbacks
It has a worst-case time complexity of O(N^2), which occur when the pivot
is chosen poorly
It is not a stable algorithm, meaning that the relative order of elements
will not be preserved in the sorted output
最糟糕的情况为O(N^2),很浪费
并不稳定,相对排序不会保留在已排序的输出中
Remark
对于非常小的数组,快速排序的性能不如插入排序,这取决于许多因素,例如进行递归调用所花费的时间、编译器等。
不要对小数组使用快速递归排序,而应该使用对小数组有效的排序算法,例如插入排序
Sort L9L10L11 | Worse | Best |
Selection sort | O(n^2) | O(n^2) |
Insertion sort | O(n^2) | O(n) |
Bubble sort | O(n^2) | O(n^2) |
Improved Bubble sort | O(n^2) | O(n) |
Merge sort | O(n log n) | O(n log n) |
Quick Sort | O(n^2) | O(n log n) |
<H3>L12 Heap Sort</H3>
<H4>引入</H4>
假设 有一家医院,他们会根据病人的年龄来接受治疗。最大的人总是第一个,不管他/她什么时候排队。 不能只跟踪最老的一个,因为如果 把他/她拉出来, 就不知道下一个最老的一个。
后面的课有tree树为什么不先讲?
<H4>Array and Binary tree</H4>
Other properties of a binary tree:
Each node can have zero, one, or two children
for a node x, we denote the left child, right child, and the parent of x as
left(x), right(x), and parent(x).
<H4>数组</H4>
是一种线性表数据结构,它使用一组连续的内存空间来存储具有相同类型的数据元素。数组中的元素可以通过下标进行访问,下标的起始位置通常从0开始。由于数组中的元素在内存中是连续存储的,因此可以通过计算内存地址来直接访问任意一个元素,这使得数组的访问时间复杂度为O(1)。数组在程序设计中非常常见,常用于存储一组有序的数据,如整数、浮点数或字符串等。
<H4>二叉树</H4>
则是一种非线性数据结构,它由一个根节点和左右两个子树组成。二叉树有多种形态,包括空二叉树、只有一个根节点的二叉树、根节点只有左子树的二叉树、根节点只有右子树的二叉树以及根节点既有左子树又有右子树的二叉树。在二叉树中,每个节点最多有两个子节点,通常被称为左子节点和右子节点。二叉树具有许多重要的性质,例如在第i层上至多有2^(i-1)个节点,深度为k的二叉树至多有2^k-1个节点等。
<H4>满二叉树</H4>
是一种特殊的二叉树,它的每一层上的节点数都是最大节点数,且所有叶子节点都在同一层上。满二叉树是完全二叉树的一种特殊情况,其中所有节点的度要么是0(叶子节点),要么是2(内部节点),不存在度为1的节点。满二叉树在数据结构和算法中具有广泛的应用,例如在堆排序、优先队列等算法中常常使用满二叉树作为底层数据结构。
<H4>堆(Heap)</H4>
是计算机科学中一类特殊的数据结构,通常可以被看作是一棵完全二叉树的数组对象。
用数组储存二叉树的堆,需要提前估计出最大堆的大小。如果多了有要求,可以那就resize数组。
二叉树不一定都是满的,这就是所谓孔(Hole)出现的原因。
<H4>Potential Problems </H4>
An estimate of the maximum heap size is required in advance in
implemented heap using an array
but we can resize the array if reeded
Side notes: it is not wise to store normal binary tree in arrays as it may
generate many holes
<H4>Maintaining the heap property: Heapify </H4>
· 计算节点i的左子节点和右子节点的索引。
· 检查左子节点的值是否大于父节点i的值,如果是,则标记左子节点为当前最大节点。
· 检查右子节点的值是否大于当前标记的最大节点(初始为父节点i或左子节点),如果是,则更新最大节点为右子节点。
· 如果最大节点不是父节点i,则交换父节点i和最大节点的值。
· 递归地对新的最大节点进行MAX-HEAPIFY操作,以确保其子树也满足最大堆的性质
<H4>Pseudocode</H4>
Build a heap from the given input array
Repeat the following steps until the heap contains only one element
Swap the root element with the last element of the heap
Remove the last element of the heap (which is now in correct position)
Heapify the remaining elements of the heap
解释如下:
第一位的数与最后一位交换,然后第一位与其子二叉树的最大值交换。
在这个例子里:17跟98换,接下来17跟2号位61、3号位75,
在二叉树此节点内内第一次发现61>17则61是当前最大;第二次发现75>61是当前最大。于是将最大3号位75和1号位17交换。同理3号位17和6号位42、7号位6的比较,将当前最大值6号位42和3号位17交换。17所在的节点没有子节点,那么这样完成了一次recursion。
完成这一过程后,10号位算已sorted。只看前未sorting的位。(Heap size=9)
接着重复以下内容:
第二轮
过程就是19跟75换。75在9号位;19跟61换,61在1号位;19跟34换,34在2号位。19在5号位。
Heap size= 8
<H4>注意的细节</H4>
Functions required
Insert new element to the end of the queue
Remove element with the highest priority from the queue
首先,我们需要理解堆的基本性质:在最大堆中,父节点的值总是大于或等于其子节点的值。
当我们要向堆中插入一个新元素时,我们通常将这个新元素添加到堆的末尾(因为堆通常使用数组来实现,末尾就是数组的最后一个位置)。但是,仅仅这样做并不足以保证堆的性质。因此,我们需要进行额外的步骤来确保堆的性质得到维护。
重新构建堆:这意味着在每次插入新元素后,我们重新构建整个堆。这种方法确实可以确保堆的性质得到维护,但它的时间复杂度是 O(n lg n),因为重新构建堆涉及到对每个元素进行下滤(percolate down)操作,而每个下滤操作的时间复杂度是 O(lg n)。对于频繁插入的场景,这种方法显然是不高效的。
就是说我们可以在插入元素时重建堆,构建回最大堆,但是重建相关于所有元素的“下滤操作”。如果有多个元素,这是不高效的。
恢复堆性质:更常见且高效的方法是只修复那些因插入新元素而可能违反堆性质的部分。具体来说,当我们向堆的末尾插入一个新元素后,我们只需要检查这个新元素是否比其父节点大。如果是,我们就交换这两个元素的位置,然后继续向上检查,直到堆的性质得到恢复或者我们到达堆的根节点。这个过程被称为“上浮”(bubble-up)或“上滤”(percolate up)。这种方法的时间复杂度是 O(lg n),因为在最坏的情况下,新元素可能需要一直上浮到堆的根节点。
所以我们就采用了这样一个恢复堆性质的原理。Heap sort算法就是采用了这一原理。
(Binary) Heap (二进制)堆
<H4>Deletion</H4>
First Attempt
1 Mark root as current node
2 Delete current node
3 An empty spot is created
4 Compare the two children of the current node
5 Bring the larger (lesser) of the two children of the current node to
the empty spot
6 Mark the larger (lesser) node as the new current node
7 Go to Step 2 until the current node has no child
这里是如何删除二叉搜索树的的过程,用法就是删除节点作为根节点,删除然后用“上浮”(bubble-up)或“上滤”(percolate up)的方法。
Remark
可以通过二叉树的方法一次性筛选出最值。
Advantages
Combine the good qualities of insertion sort (sort in place) and merge
sort (speed, O(n log n) in all case)
Memory usage can be minimal if implemented with iterative heapify
(instead of recursive one)
Simpler to understand than other equally efficient sorting algorithms
because it does not use advanced computer science concepts such as
recursion
结合insertion和merge sort的质量
迭代堆实现,内存使用小。(非递归)
不使用先进实现方法,更容易理解
Drawbacks
Costly: since the constants are higher compared to merge sort even if
the time complexity are the same
Unstable: it might rearrange the relative order
Efficient: not very efficient when working with highly complex data
代价高
时间复杂度不变
不稳定
重排相对顺序高效
高度复杂效率一般
<H2>Tutorial 01 Recursive & Code Tracing</H2>
<H3>问题1 爬楼梯的递归</H3>
1.Suppose you are climbing the stairs. It takes you n steps to get to the roof (n ≥ 1). You can climb 1 or 2 steps at a time. How many different ways can you get to the top?
爬楼梯问题,一次走1或2阶, 有几种方法到达?
(a)Consider the base case. How many ways can you get to the first step? How about the second step?
第一步只有一种方法,第二步有两种。
- To get to the first step, there's only one way: taking one step.
- To get to the second step, there are two ways: taking one step twice or taking two steps at once.
(b)Consider the state transition. Suppose there are f(n − 2) ways to get to the (n − 2)th step, and f(n − 1) ways to get to the (n − 1)th step, how many ways can we get to the n th floor? n层的方法必然是从n-2跨2步来的,或者是n-1跨1步来的,所以组成了到n层的递归方法
f(n)=f(n-1)+f(n-2);
Compute f(n) with f(n − 2) and f(n − 1).
To get to the n-th step, you can either come from the (n-1)-th step and take one step, or you can come from the (n-2)-th step and take two steps. Therefore, the number of ways to get to the n-th step, f(n), is the sum of the ways to get to the (n-1)-th step, f(n-1), and the ways to get to the (n-2)-th step, f(n-2).
(c) Complete the following code with above conclusions.
public int climbStairs(int n) {
if (n == 1) return 1; // Only one way to climb 1 step
if (n == 2) return 2; // Two ways to climb 2 steps
return climbStairs(n-1) + climbStairs(n-2);
// Recursive call to calculate the number of ways
}
(d)To find the minimum cost to reach the top of the stairs, we need to consider the cost associated with each step. Since we can either climb one or two steps at a time, the minimum cost to reach the n-th step will be the minimum of the cost to reach the (n-1)-th step plus the cost of the n-th step, and the cost to reach the (n-2)-th step plus the cost of the n-th step and the (n-1)-th step.
To optimize the above implementation, we can use memoization or dynamic programming to avoid redundant recursive calls and improve performance. Here's a dynamic programming implementation:
public int minCostClimbingStairs(int[] cost) {
int n = cost.length;
int[] dp = new int[n + 1]; // dp[i] represents the minimum cost to reach the i-th step
// Base cases
dp[0] = 0; // Starting from step 0, cost is 0
dp[1] = cost[0]; // Reaching the first step has the cost of the first step
// Fill in the dp array from the second step to the last step
for (int i = 2; i <= n; i++) {
dp[i] = cost[i - 1] + Math.min(dp[i - 1], dp[i - 2]);
}
// The minimum cost to reach the top of the stairs is in dp[n]
return dp[n];
}
public int minCostClimbingStairsRecursive(int[] cost, int n) {
if (n <= 2) {
// Base cases: for 1 or 2 steps, return the respective cost
return (n == 1) ? cost[0] : Math.min(cost[0], cost[1]);
}
// Recursive calls to find the minimum cost to reach the (n-1)-th and (n-2)-th steps
int costToReachNMinus1 = minCostClimbingStairsRecursive(cost, n - 1);
int costToReachNMinus2 = minCostClimbingStairsRecursive(cost, n - 2);
// The minimum cost to reach the n-th step is the minimum of two options:
// 1. Take one step from the (n-1)-th step, add the cost of the n-th step
// 2. Take two steps from the (n-2)-th step, add the cost of the (n-1)-th and n-th steps
return cost[n - 1] + Math.min(costToReachNMinus1, cost[n - 2] + costToReachNMinus2);
}
In this dynamic programming approach, we iterate from the base cases to the target step, storing the minimum cost to reach each step in the dp array. Finally, we return dp[n], which represents the minimum cost to reach the top of the stairs.
<H3>Code Tracing </H3>
<H4>问题2 气泡排序问题</H4>
The following method implements a bubble sort algorithm.
public void bubbleSort(int[] array){
for(int i=0; i<array.length; i++){
for(int j=0; j<array.length-1; j++){
if(array[j]>array[j+1]){
int tmp = array[j+1];
array[j+1] = array[j];
array[j] = tmp;
}
}
}
}
(a) Given the input array as [10, 5, 4, 3], trace the array after each iteration
i | j | array |
0 | 0 | 5,10,4,3 |
0 | 1 | 5,4,10,3 |
0 | 2 | 5,4,3,10 |
1 | 0 | 4,5,3,10 |
1 | 1 | 4,3,5,10 |
1 | 2 | 4,3,5,10 |
2 | 0 | 3,4,5,10 |
2 | 1 | 3,4,5,10 |
2 | 2 | 3,4,5,10 |
3 | 0 | 3,4,5,10 |
3 | 1 | 3,4,5,10 |
3 | 2 | 3,4,5,10 |
(b) In terms of the above bubble sort algorithm, given an array with length as n, for each iteration of the outer loop, how many times does the inner loop iterate? Observing the table, is it necessary for the inner loop to iterate so many times? If not, try to find the minimum number of iterations of the inner loop, and ensure that any array can be sorted successfully under this number of iterations (Suppose the index of outer loop is i).
这里就在问bubble的优化模式。
Best | Worse | |
Improved Bubble sort | O(n^2) | O(n) |
<H4>问题3 算法的改进和优化</H4>
3.The following code is intended to find the position of the number equals to the target within an ascending array.
public int binarySearch(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = (right - left) / 2 + left;
int num = nums[mid];
if (num == target) {
return mid;
} else if (num > target) {
right = mid;
} else {
left = mid;
}
}
return -1;
}
Trace the code with nums as [2, 4, 5, 7, 10, 13] and target as 13, what errors do you find, and how you fix the code?
public int firstLarger(int[] nums, int left, int right, int target) {
//元素个数太少的特殊条件
if (left > right) {
// 如果 left 大于 right,说明搜索区间为空,没有找到大于 target 的元素
return -1;
}
if (right - left == 0&&nums[left] > target) {
// 如果搜索区间只剩下一个元素,直接比较该元素和目标值
return left;
}else{
return -1;
}
int mid = left + (right - left) / 2;
if (nums[mid] <= target) {
// 如果中间元素不大于目标值,说明第一个大于目标值的元素在 mid 的右侧 return firstLarger(nums, mid + 1, right, target);
} else { // 如果中间元素大于目标值,需要判断它是否是第一个大于目标值的元素
if (mid == 0 || nums[mid - 1] <= target) {
// 如果是数组的第一个元素,或者前一个元素不大于目标值,那么 mid 就是第一个大于目标值的元素
return mid;
} else { // 否则,第一个大于目标值的元素在 mid 的左侧
return firstLarger(nums, left, mid - 1, target);
}
}
}
<H4>问题4 搜索大于目标的第一个数字</H4>
4. The following method ‘search’ is intended to find the position of the first number that larger than the target within an ascending array (e.g., given an array [0, 2, 5, 7] and target as 3, the method should output 2 since 5 is the first element that larger than 3).
(a) Tracing the code:
(1) nums as [-1, 0, 3, 5, 9, 12] and target as 8;
Let's trace the code step by step:
- search method is called with nums and target values.
- firstLarger method is called with left = 0, right = 5, and target = 8.
- mid is calculated as (5-0)/2 + 0 = 2, so mid = 2.
- nums[mid] = 3, which is less than or equal to target (8).
- Code proceeds to line 14 and calls firstLarger with left = mid+1 = 3, right = 5, and target = 8.
- mid is now calculated as (5-3)/2 + 3 = 4.
- nums[mid] = 9, which is greater than target (8).
- Code proceeds to line 16 and checks nums[mid-1] = 5, which is less than or equal to target (8).
- Line 17 is executed and the method returns mid = 4, which is the index of the first element larger than 8.
(2) nums as [1, 3, 4] and target as 0;
- search method is called with nums and target values.
- firstLarger method is called with left = 0, right = 2, and target = 0.
- mid is calculated as (2-0)/2 + 0 = 1.
- nums[mid] = 3, which is greater than target (0).
- Code proceeds to line 16 and checks nums[mid-1] = 1, which is greater than target (0).
- Line 17 is not executed because nums[mid-1] is not less than or equal to target.
- Code proceeds to line 19 and calls firstLarger with left = 0, right = mid-1 = 0, and target = 0.
- This time, the base case is triggered (nums.length == 1) and nums[0] = 1 is greater than target (0).
- Line 8 is executed and the method returns 0, which is the index of the first element larger than 0.
Error and Fix:
The code is working correctly for the given examples. However, there is a potential issue with the base case handling. In the current implementation, if nums.length == 1 and the only element is less than or equal to target, the method incorrectly returns -1, indicating that there is no element larger than target. This is incorrect because the single element, even if it's not larger than the target, is still the first (and only) element of the array. The method should return 0 in this case.
To fix this, you should change lines 7-10 to:
if (nums.length == 1){
if (nums[0] > target)
return 0;
else
return -1; // This should be changed to return 0.
}
To:
if (nums.length == 1){
return (nums[0] > target) ? 0 : -1;
}
Or simply:
if (nums.length == 1){
return (nums[0] > target) ? 0 : nums.length; // Return the length of the array to indicate no larger element found.
}
Returning the length of the array (nums.length) can be a convention to indicate that no larger element was found within the given array.
(b) Time Complexity:
The firstLarger method uses a binary search approach to find the first element larger than the target. In each recursive call, the search space is halved, resulting in a logarithmic time complexity. The time complexity of this method is O(log n), where n is the length of the input array nums.
<H4>附运行实例</H4>
以下是这个
sort
的运行代码
package ttlsort;
public class TTLsort {
public int search(int[] nums, int target) {
return firstLarger(nums, 0, nums.length-1, target);
}
public int firstLarger(int[] nums, int left, int right, int target) {
//
元素个数太少的特殊条件
if (left > right) {
//
如果
left
大于
right
,说明搜索区间为空,没有找到大于
target
的元素
return -1;
}
int mid = left + (right - left)/2;
if (right - left == 0&&nums[left] > target) {
//
如果搜索区间只剩下一个元素,直接比较该元素和目标值
return left;
}else{
return -1;
}
if (nums[mid] <= target) {
//
如果中间元素不大于目标值,说明第一个大于目标值的元素在
mid
的右侧
return firstLarger(nums, mid + 1, right, target);
} else { //
如果中间元素大于目标值,需要判断它是否是第一个大于目标值的元素
if (mid == 0 || nums[mid - 1] <= target) {
//
如果是数组的第一个元素,或者前一个元素不大于目标值,那么
mid
就是第一个大于目标值的元素
return mid;
} else { //
否则,第一个大于目标值的元素在
mid
的左侧
return firstLarger(nums, left, mid - 1, target);
}
}
}
public static void main(String[] args) {
int[] nums = {2, 4, 5, 7, 10, 13};
TTLsort ttlSort = new TTLsort(); //
创建
TTLsort
类的实例
int target = 13; //
假设我们要查找的目标是
10
int index = ttlSort.search(nums, target); //
调用
binarySearch
方法
System.out.println("Index of target " + target + ": " + index);
}
}
<H2>Assignment1</H2>
<H3>Problem 1 对数运算</H3>
<H3>Problem 2 多项式的求值算法</H3>
You are given the coefficients a0, . . ., an of a polynomial
and you want to evaluate this polynomial for a given value of x. Horner’s rule says to evaluate the polynomial according to this parenthesization:
The procedure HORNER implements Horner’s rule to evaluate P(x), given the coefficients a0, . . ., an in an array A[0 : n] and the value of x
- In terms of θ-notation, what is the running time of this procedure?
假设算术可以在恒定时间内完成(Θ(n),其中c是常数),并且由于循环被执行n次,因此,HORNER的运行时间为Θ(n)。
简单For 循环,复杂程度是Θ(n)
- Write pseudocode to implement the naive polynomial-evaluation algorithm that computes each of the polynomial from scratch. What is the running time of this algorithm? How does it compare with HORNER?
NAIVEHORNER(A, n, x)
1 p = 0
2 for i = 0 to n
3 yi = 1
4 for j = 1 to i
5 yi = yi ∗ x
6 p = p + A[i] ∗ yi
7 return p
Since both the inner loop and the outer loop are being executed Θ(n)-times, the running time of NAVIEHORNER is Θ(n 2 ). Therefore, it is slower than HORNER’s rule.
<H3>Problem 3 分而治之</H3>
The following recurrence often arises in divide-and-conquer algorithms,
where the processes of dividing and combining involves sorting.
where n is a power of 2. Prove that T (n) = Θ(nlg2n).
<H3>Problem 4 两种排序的比较</H3>
Suppose that for inputs of size n on a particular computer, insertion sort runs in 8n2 steps and merge sort runs in 64n lg n steps. For which values of n does insertion sort beat merge sort? (Hint: You may need to test different values of n to find the correct answer.)
<H3>Problem 5 插入排序</H3>
Here is an array of ten integers:
5 | 3 | 8 | 9 | 1 | 7 | 0 | 2 | 6 | 4 |
Draw this array after the FIRST iteration of the large loop in an insertion sort
(sorting from smallest to largest).
<H3>Problem 6 快速排序</H3>
Here is an array which has just been partitioned by the first step of quick sort:
3 | 0 | 2 | 4 | 5 | 8 | 7 | 6 | 9 |
Which of these elements could be the pivot? (There may be more than one possi-bility!)
Both 4, 5 and 9 could be used as the pivot
Pivot 所有的前面的数都比Pivot小,所有后面的数都比Pivot大。这就是快速排序的目的,需要让选择Pivot和其他数比大小。
<H3>Problem 7 排序的选用</H3>
Banks often record transaction on an account in order of the times of the
transactions, but many people like to receive their bank statements with checks listed in order by check number. People usually write checks in order by check number, and merchants usually cash them with reasonable dispatch. The problem of converting time-of-transaction ordering to check-number ordering is therefore the problem of sorting almost-sorted input. Explain persuasively why the procedure INSERTIONSORT might tend to beat the procedure QUICKSORT on this problem.
银行支票处于差不多排序好的号码,所以需要插入排序的微调而不是大范围的快速牌序。
Suppose that it takes c number of steps for a person to insert the check to its correct location. To sort n number of checks, the running time of INSERTION-SORT is O(cn) = O(n). In QUICKSORT, however, as c is usually small, the split of PARTITION will be at best n − c, which may leads to O(n 2 ) running time.
<H3>Problem 8 Merge sort</H3>
Here is an array of eight integers:
5 | 3 | 8 | 9 | 1 | 7 | 0 | 2 |
Draw this array after the TWO recursive calls of merge sort are completed, and before the final merge step has occurred.
<H3>Problem 9 递归</H3>
Consider the following recursive program.
(a)What is the output for foo(2,7)?
foo(2,7)
if (7 % 2 == 1) {
y = foo(2, (7 - 1) / 2);
foo(2,3)
if (3 % 2 == 1) {
y = foo(2, (3 - 1) / 2);
foo(2,1)
if (1 % 2 == 1) {
y = foo(2, (1 - 1) / 2);
foo(2,0)
return 1
Back foo(2,1)
y = foo(2, (1 - 1) / 2)=1
return 2
Back foo(2,3)
y = foo(2, (3 - 1) / 2)=2
return 8
Back foo(2,7)
y = foo(2, (7 - 1) / 2)=8
Return x*y*y=128
(b)What does the program do?
The program calculate the value of x n.
(c)Analyze the worst-case running time of the program using Big-Oh (O), assuming that n is a power of 2. (Show all steps.)
n/2^k 接近或等于 1 的最小整数。换句话说,k 近似于 log_2 n。
理解这一点,就知道怎么推出这一答案。
注意O(lgn)其实是2为底的log.
<H3>Problem 10 Heap Sort</H3>
- Does the following array represent a min-heap (Yes/No) ?
3 | 6 | 7 | 23 | 41 | 19 | 27 | 29 |
- Given the following max-heap of size 10
45 | 37 | 29 | 20 | 18 | 27 | 13 | 4 | 8 | 14 |
Delete the maximum from the max-heap and give the resulting max heap in a form of an array.
<H2>Lecture 13 Non-comparison Based Sorting</H2>
Counting Sort and Radix Sort
Merge sort and Quick sort worst-case running time is O(n lg n) and O(n^2), respectively
Is there a better algorithm than these two algorithms?
Non-comparison-based sorting algorithms
Work well when there is limited range of “input values”
A sorting algorithm that sorts the elements by counting the number of occurrences of unique element, known as keys, in the array.
Counting sort
<H3>Counting sort </H3>
· 找出待排序的数组中最大和最小的元素:这一步是为了确定计数排序中计数数组的大小。
· 统计数组中每个值为i的元素出现的次数,存入计数数组的第i项。
· 对所有的计数累加(从计数数组中的第一个元素开始,每一项和前一项相加)。
· 反向填充目标数组:将每个元素i放在新数组的第count[i]项,每放一个元素就将count[i]减去1
A
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
3 | 2 | 0 | 3 | 6 | 2 | 0 | 6 | 2 | 4 |
C
0 | 1 | 2 | 3 | 4 | 5 | 6 |
2 | 0 | 3 | 2 | 1 | 0 | 2 |
对上面的进行计数
C update C一般指这个累加过的C
<=
0 | 1 | 2 | 3 | 4 | 5 | 6 |
2 | 2 | 5 | 7 | 8 | 8 | 10 |
然后进行逆向 排序
A[10]=4 C[4]=1 Cup[4]=8 Cup[4]-1=Cup[4] B[8]=4 | A[9]=2 C[2]=3 Cup[2]=5 Cup[2]-1=Cup[2] B[5]=2 | A[8]=6 C[6]=1 Cup[6]=10 Cup[6]-1=Cup[6] B[10]=6 | A[7]=0 C[0]=2 Cup[0]=2 Cup[0]-1=Cup[0] B[2]=0 | A[6]=2 C[2]=3 Cup[2]=4 Cup[2]-1=Cup[2] B[5]=2 | A[5]=6 C[6]=2 Cup[6]=9 Cup[6]-1=Cup[6] B[9]=6 | A[4]=3 C[3]=2 Cup[3]=7 Cup[3]-1=Cup[3] B[7]=3 |
A[3]=0 C[0]=2 Cup[0]=1 Cup[0]-1=Cup[0] B[1]=0 | A[2]=2 C[2]=3 Cup[2]=3 Cup[2]-1=Cup[2] B[3]=2 | A[1]=3 C[3]=2 Cup[3]=7 Cup[3]-1=Cup[3] B[6]=3 |
B
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
0 | 0 | 2 | 2 | 2 | 3 | 3 | 4 | 6 | 6 |
非负整数假设:计数排序的一个基本前提是,输入的元素都是非负整数,或者至少可以映射为非负整数。这是因为计数排序通过将这些整数作为索引来填充辅助数组,而数组的索引在大多数编程语言中都是非负的。
范围从0到k:这里提到的“k”是计数排序中计数数组的大小,它必须能够覆盖输入元素中可能出现的最大值。如果输入元素范围不是从0开始,或者包含了超出计数数组范围的值,那么算法将不会正确工作。
不进行比较:这是计数排序的一个显著特点。它不需要像快速排序、归并排序等算法那样通过比较来确定元素的顺序。相反,计数排序利用计数数组来直接确定每个元素在输出数组中的位置。
总的来说,计数排序是一种针对特定情况(即非负整数或可映射为非负整数的输入)非常高效的排序算法。它的时间复杂度是线性的,但需要额外的空间来存储计数数组,因此在处理大规模数据或内存受限的情况下可能不是最佳选择。然而,在知道输入数据范围且内存充足的情况下,计数排序是一种简单且高效的排序方法。
<H4>算法复杂度(时间复杂度)</H4>
计数排序的时间复杂度是线性的,具体地说,它的时间复杂度是O(n + k),其中n是输入元素的数量,k是输入元素范围的上限(即计数数组的大小)。
- O(n): 遍历输入数组,统计每个元素出现的次数。
- O(k): 初始化并填充计数数组,以及对计数数组进行累加操作。
近似为O(n)因为k小;如果O(n+k)因为k大。
<H4>空间复杂度
计数排序的空间复杂度是O(k),其中k是输入元素范围的上限。这是因为需要一个大小为k的计数数组来存储每个元素的出现次数。
<H4>一些灵活应用</H4>
在计数排序中,它需要构建一个等于该键的最大值,即:
City Population | City Population |
Ahmedabad | 7 681 000 |
Alexandria | 5 086 000 |
Atlanta | 5 572 000 |
Baghdad | 6 812 000 |
Bangalore | 11 440 000 |
O(n)=k
使用输入元素作为索引:计数排序的核心思想是利用输入元素作为辅助数组的索引,从而避免了传统的比较操作。这种非比较特性使得计数排序在处理一定范围内的整数时非常高效。
<H4>优势</H4>
时间复杂度O(n+k)
易于编码 逻辑简单
稳定性好 顺序不变
<H4>劣势</H4>
1.不适用于小数值:
1.计数排序假设输入元素是非负整数,或者可以映射为非负整数。因此,它不适用于包含小数或负数的输入。
2.输入值范围大时效率低:
1.如果输入元素的范围(k)非常大,计数排序需要分配大量的额外空间来存储计数数组,这会导致空间效率降低。
2.同时,处理大范围的计数也会增加算法的时间复杂度,使其可能不如其他排序算法高效。
3.不是原地排序算法:空间有限不能用。
<H3>Radix Sort</H3>
基数排序
Commonly use as a way to sort punch cards as early as 1923
Can be implemented to start with
Least significant digit (LSD)
Most significant digit (MSD)
</H4>Pseudocode</H4>
找到数组中的最大元素:
这一步是为了确定需要排序的最大位数,因为基数排序是按位进行的。
找到最大元素的位数:
这一步确定了需要迭代排序的次数,因为要对每一位数字都进行排序。
循环处理每一位:
从最低位开始,一直到最高位,对每一位数字进行排序。
使用稳定排序算法排序每一位:
基数排序要求使用稳定排序算法,因为稳定排序算法在排序时不会改变相等元素的相对顺序。这样,当对更高位进行排序时,先前排序的结果不会被打乱。
处理位数不足的情况:
如果某个元素的位数少于当前正在排序的位数,那么在该元素的前面补0。这是为了确保排序算法能够正确处理所有元素。
总结:
补0保持同位,最后一位开始按该位大小排序,在同一组的顺序由原来的顺序决定,重复直到每一位执行该操作。
<H4>复杂度</H4>
时间复杂度⇒ the overall time complexity is Θ(d(n + k)) d:最长位数
基数排序的空间复杂度可以大致认为是 O(n + k),
其中 n 是待排序元素的数量,k 是数字可能取到的最大值(如果使用计数排序作为稳定排序算法)。如果 k 是一个常数或增长缓慢,则空间复杂度可以简化为 O(n)。然而,如果 k 与 n 成正比或更快增长,则空间复杂度会更高。
<H4>Advantages </H4>
In practice, radix sort is often faster than other comparison-based sorting
algorithms, such as quick sort and merge sort, for large datasets,
especially when the keys have many digits
基数排序通常比其他基于比较的排序更快算法 线性时间复杂度
相同数的顺序会被保留 稳定排序
依赖位数 适用于整数排序
<H4>Drawbacks </H4>
Its time complexity grows linearly with the number of digits, and so it is
not as efficient for small datasets
It requires fixed size keys to operate, which may not be the case for some
types of data
It is not an in-place algorithm as it uses a temporary count array
It requires an auxiliary space of O(n + k) for creating the buckets for
each digit value and to copy the elements back to the original array after
each digit has been sorted.
需要预处理,提前给缺位数的数前面加0
时间复杂度随数字呈线性增长
小的数据集和非整数或非正数没有那么有效
它需要固定大小的键来操作
不是原地算法需要辅助空间
<H2>Lecture 14 Linked Lists</H2>
常见数据储存的方式 数组,链表
数组
链表
为什么要用链表,因为数组存在弊端
<H3>
Disadvantages of Arrays </H3>
Fixed size inefficient in adding new elements to, or removing
elements from, the arrays
1.大小固定2.增减元素不方便
Most programming languages require each element of a particular
array to be the same size
3.元素大小必须相同
▶ In the case of large number of records, it may take more space than
required for storing the same information
4.空间上的浪费
In some programming languages, such as C and C++, the compiler
doe not execute index bound checking, and a run time error may
appear
5.索引越界错误:引用一些错误的不存在的索引
Have limited functionality compared to other data structures
6.功能有限: 与其他数据结构(如链表、树或图)相比,不能快速插入和删除操作,也不支持像树或图那样的复杂查询和遍历操作。
<H3>
链表的基础
</H3>
<H4>
基本类型</H4>
单链表
双链表
循环链表
还有很多...
<H4>
单链表的四种基本操作</H4>
插入(insert)
删除(delete)
查找(遍历)(search)
更新(update)
在本节课的实验中,我们将熟练运用这四种操作。
<H4>
链表的基本思想</H4>
链表是一系列连接的元素(或节点)
A linked list is a series of connected elements (or nodes)
序链型数列 数据+指向下一个链表节点的指针
The link is used to chain the data
Head: pointer to the first element
Tail: the last element of the list with its link points to null
Node:节点
一个节点的构成
public class Node {
int n;
Node next;
}
Link:连接(用指针)
链表的操作
The list can grow and shrink 链表变长/短的方法
Add有三种方式:添加到头部(头部插入)、添加到尾部(尾部插入)和添加到中间(指定位置插入)。
Delete有两种方式:删除头部元素和删除指定值的元素。在某些情况下,可能还需要实现删除指定位置的元素。
这是一个链表的node
public class Node {
int n;
Node next;
}
这是一个链表的尾部node
Node node = new Node();
node.next = null
检查终止
if (node.next == null) {
...
}
链表:理想的表操作
一些基础的接口定义
Insert a node into a list 规范接口的定义
▶ In front of the list
▶ In the middle of the list (after some node)
▶ At the end of the list (i.e., append)
Delete a node from the list
Get the size/length of the list
Find the leading node in the list
Find the last node in the list
Find whether a node is in the list
Traverse the list
Enumerate all nodes in the list
<H4>
链表的基础接口</H4>
1.insert_head(Node node):
方法作用:在链表的头部插入一个节点。
参数:一个待插入的节点(Node)。
返回值:通常返回插入操作的状态,例如成功返回1,失败返回0或其他错误码。
2.insert(Node node, int ind):
方法作用:在链表的指定位置插入一个节点。
参数:一个待插入的节点(Node)和插入位置的索引(int ind)。
返回值:通常返回插入操作的状态。
3.append(Node node):
方法作用:在链表的尾部(即末尾)添加一个节点。
参数:一个待添加的节点(Node)。
返回值:通常返回添加操作的状态。
4.set(Node node, int ind):
方法作用:将指定位置的节点替换为给定的节点。
参数:一个待替换的节点(Node)和替换位置的索引(int ind)。
返回值:通常返回替换操作的状态。
5.remove(Node node):
方法作用:从链表中删除指定的节点。
参数:待删除的节点(Node)。
返回值:通常返回删除操作的状态。注意,实际实现中可能需要传递节点的值或者某种形式的引用,因为直接传递节点本身可能不足以在链表中定位它。
6.size():
方法作用:返回链表的长度或大小(即节点数量)。
返回值:链表的节点数量。
7.isEmpty():
方法作用:检查链表是否为空。
返回值:如果链表为空,返回true;否则返回false。
8.first():
方法作用:获取链表的第一个节点。
返回值:链表的头节点(如果存在)。
9.last():
方法作用:获取链表的最后一个节点。
返回值:链表的尾节点(如果存在)。
10.get(int i):
方法作用:根据索引获取链表中的节点。
参数:索引值(int i)。
返回值:位于指定索引位置的节点。如果索引超出范围,可能返回null或抛出异常。
11.contains(Node node):
方法作用:检查链表中是否包含指定的节点。
参数:待检查的节点(Node)。
返回值:如果链表包含该节点,返回true;否则返回false。
12.clear():
方法作用:清空链表,移除所有节点。
返回值:通常没有返回值,因为清空操作本身没有太多状态可以返回。
<H4>
链表的具体实现</H4>
LinkedList 类
实现了之前定义的 List 接口,并提供了链表的具体实现。
public LinkedList() {
head = null; // 链表头部初始化为null
tail = null; // 链表尾部初始化为null,以便快速追加节点
size = 0; // 链表大小初始化为0
}
构造函数初始化链表的三个重要属性:head(头节点)、tail(尾节点)和size(链表大小)。开始时,链表是空的,所以head和tail都是null,size是0。
插入节点到头部 (insert_head)
public int insert_head(Node node) {
node.next = head; // 新节点的下一个节点指向当前的头节点
head = node; // 更新头节点为新节点
if (tail == null) {
tail = node; // 如果链表原本为空(tail为null),则新节点也是尾节点
}
size++; // 链表大小加1
return size; // 返回更新后的链表大小
}
追加节点到尾部 (append)
public int append(Node node) {
if (tail != null) {
tail.next = node; // 尾节点的下一个节点指向新节点
} else { // 链表为空
head = node; // 头节点指向新节点
}
node.next = null; // 新节点的下一个节点设置为null,因为它是尾节点
tail = node; // 更新尾节点为新节点
size++; // 链表大小加1
return size; // 返回更新后的链表大小
}
节点类 (Node)
public class Node {
int data; // 假设节点存储整型数据
Node next; // 指向下一个节点的引用
public Node(int data) {
this.data = data;
}
}
<H4>
链表的遍历和搜索
</H4>
这个search方法中,目的是从链表的头部开始,逐个节点进行遍历,直到找到与给定节点相等的节点或者遍历到链表的末尾。
下面将详细分析和讲解这个方法。
public int search(Node node) {
Node currNode = head; // 从链表的头节点开始遍历
while (null != currNode && !currNode.equals(node)) {
currNode = currNode.getNext(); // 移动到下一个节点
}
if (null == currNode) return null; // 如果遍历到末尾仍未找到,返回null
return currNode.equals(node) ? currNode : null; // 如果找到节点,返回该节点,否则返回null
}
提示
currNode.equals(node) ? currNode : null;
运用了一个三元运算符
X?A:B
· 首先计算X条件(在这里是 currNode.equals(node)
)。
· 如果条件为 true
,则运算符的结果是 表达式
A
的值(在这里是 currNode
)。
· 如果条件为 false
,则运算符的结果是 表达式
B
的值(在这里是 null
)
这个三元运算符被用来检查当前节点 currNode
是否等于目标节点 node
。如果是,就返回当前节点;如果不是(或者链表已经遍历完),就返回 null
。
分析
1.初始化当前节点:Node currNode = head;
这行代码将当前节点currNode
初始化为链表的头节点head
。这是开始遍历的起点。
2.遍历链表:while
循环用于遍历链表。
条件null != currNode && !currNode.equals(node)
表示只要当前节点不为null
且当前节点不等于要搜索的节点,就继续遍历。currNode.getNext();
用于获取当前节点的下一个节点,并将当前节点移动到下一个节点。
3.返回结果:如果遍历到链表末尾(currNode
为null
),
则方法返回null
,表示未找到目标节点。
如果找到了目标节点(即currNode.equals(node)
为true
),则应该会返回true
或1
。
检查节点是否为null
是非常重要的,因为在遍历链表时, 需要知道何时到达链表的末尾。链表的末尾通常由null
指针表示,这意味着没有更多的节点可以遍历。如果不检查null
,尝试访问一个不存在的下一个节点(即null
引用)将会导致NullPointerException
,这是一个运行时错误。因此,在遍历链表时,检查当前节点是否为null
是一个标准的做法,以确保程序的正确性。
<H4>
链表的指定位置插入
</H4>
Linked list: Insert node at a specified location
<H3>Variants of Linked lists</H3>
<H4>Circular linked lists</H4>
The last node points to the first node of the list, creating a cycle,
like a circular array. How do we know when we have finished traversing the list?
▶ Tip: check if the pointer of the current node is equal to the head
<H4>Doubly linked lists</H4>
Each node points to not only successor, but the predecessor
There are two null pointers: one at the first node and another one
at the last node in the list
Advantage: given a nodem it is easy to visit its predecessor.
Convenient to traverse lists backwards
<H3>Arrays vs Linked lists</H3>
Linked lists | Arrays | |
Data structure | Non-contiguous | Contiguous |
Memory allocation | Dynamic | Static (fixed) |
Access Coding and Manage | Sequential Quite primitive! Need to define every operation by yourself! | Random Easy |
Insertion/Deletion | Efficient - only need to reset some pointers, no need to move other nodes | Inefficient - may need to resize the array to make room for new elements or close the gap caused by deleted elements |
time complexity
Operation | Linked lists | Arrays |
Get | O(n) | O(1) |
Size | O(1) | ? (not the length!) |
Set value to a particular location | O(n) | O(1) |
Add(ind,value) | O(1) – insert head or append O(n) – otherwise | ? (where is the last value? What happens if its full? and have to shifting up!) |
Remove(ind) | O(n) | ? (have to shift everything down!) |
Remove(value) | O(n) | ? (have to find the value, then shift everything down!) |
<H2>Laboratory 3 Linked Lists</H2>
这是一个关于链表处理的编程算法的练习。实现了单链表的一些基本功能。
Node.java
An element inside the LinkedList,
SortedList.java
Interface of the SortedLinkedList class,
SortedLinkedList.java
The class you are going to implement,
<H3>Node.java </H3>
(本文件已给出)
import java.util.Objects;
public class Node {
private int data;
private Node next;
/*
data:一个私有整数变量,用于存储节点中的数据。
next:一个指向下一个Node对象的私有引用。
*/
public Node(int data) {
setData(data);
next = null;
}
/*
这是一个带有一个整数参数的构造函数,用于创建一个新的Node对象。
它首先调用setData方法设置节点的数据。
然后将next引用初始化为null,表示这个节点目前没有下一个节点。
*/
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
/*
getData 和 setData:用于获取和设置节点的data值。
getNext 和 setNext:用于获取和设置节点的next引用。
*/
@Override
public int hashCode() {
return Objects.hash(data);
}
/*
这个方法重写了Object类的hashCode方法。
它使用data成员变量的值来计算节点的哈希码。
暂时用不上
*/
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
Node other = (Node) obj;
return data == other.data;
}
/*
这个方法重写了Object类的equals方法。
它首先检查传入的对象是否与当前对象相同(内存地址相同),如果是,则返回true。
然后检查传入的对象是否为null,如果是,则返回false。
接着,它检查传入的对象是否属于相同的类(即它们是否都是Node对象)。如果不是,则返回false。
最后,它将传入的对象强制转换为Node类型,并比较它们的data值是否相同。
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(data) //
// .append(" (").append(getClass().getSimpleName()).append(")") //
;
return sb.toString();
}
/*
这个方法重写了Object类的toString方法。
它创建一个StringBuilder对象,并附加节点的data值。
注释掉的代码段用于附加节点的类名(例如"Node")。
最后,返回生成的字符串。
*/
}
<H3>SortedList.java</H3>
这个文件是来构造接口用的。具体作用已给出。本次lab的目标就是让这些接口起作用。
public interface SortedList {
/**
* Add node to the list 向链表中添加节点
*
* @param value Value of node to be added 要添加的节点的值
* @return index of the node in the list 返回链表中的节点的索引
*/
int add(int value);
/**
* Remove the node at index <code>ind</code> 删除索引<code>ind</code>处的节点
*
* @param <code>ind</code> Index of the node to be removed
* 要删除的节点的<code>ind</code>索引
* @return index of the node removed 返回已删除的节点的索引-、
*/
int remove(int ind);
/**
* Remove the node with the same content as the input node
* 删除与输入节点具有相同内容的节点
* @param node Node contains the content to be removed
* 节点包含要删除的内容
* @return index of the node removed; -1 if no node has been removed
* 返回已删除节点的索引;如果未删除任何节点,则为-1
*/
int remove(Node node);
/**
* Size of the list
* 链表的大小
* @return Size of the list
* 链表的大小
*/
int size();
/**
* Check if the list is empty
* 检查该链表是否为空
* @return true if the list is empty; <code>false</code> otherwise
* 如果链表为空,则返回true;否则为<代码>false</code>
*/
boolean isEmpty();
/**
* Return the first element of the list
* 返回该链表中的第一个元素
* @return First element of the list; NULL is the list is empty
* 返回链表的第一个元素;NULL是链表为空
*/
Node first();
/**
* Return the last element of the list
* 返回链表中的最后一个元素
* @return Last element of the list; NULL if the list is empty
* 返回链表中的最后一个元素;如果链表为空,则为NULL
*/
Node last();
/**
* Return the i-th element of the list
* 返回链表中的第i个元素
* @param ind index of the element to be returned
* 要返回的元素的ind索引
* @return i-th element in the list; NULL if the list is empty
* 链表中的第i个元素;如果链表为空,则为空
*/
Node get(int ind);
/**
* Determine if the list contains the input node
* 确定该链表中是否包含该输入节点
* @param value Vale of node to be checked
* 要检查节点的值
* @return The node contains the specified value; NULL otherwise
* 该节点包含指定的值,否则为NULL
*/
Node contains(int value);
/**
* Clear the list.
* 清除链表。
* I.e., remove all element in the list
* 即,删除链表中的所有元素
*/
void clear();
/**
*清理链表
*/
}
<H3>SortedLinkedlist.java</H3>
package xjtlu.cpt108.collections.linkedList;
public class SortedLinkedList implements SortedList {
private Node head;
private Node tail;
private int size;
public SortedLinkedList() {
head = null;
tail = null;
size = 0;
}
/**
*初始化链表
*/
@Override
public int add(int value) {
System.out.printf(String.format("add(%s)",value));
int currLoc=0;
Node newNode=new Node(value);
if(null==head){//Empty list condition
head = tail = newNode;
}else if(head.getData() >= value){//
newNode.setNext(head);
head = newNode;
currLoc = size;
}else{
Node currNode = head;
Node prevNode = null;
while(currNode != null && value > currNode.getData()){
prevNode = currNode;
currNode = currNode.getNext();
}
newNode.setNext(prevNode.getNext());
prevNode.setNext(newNode);
if(null == currNode) tail = newNode;
}
size++;
return currLoc;
}
@Override
public Node get(int ind) {
if (ind < 0 || ind >= size) {
throw new IllegalArgumentException("Error index!");
}
Node currNode = head;
for (int i = 0; i < ind; i++) {
currNode = currNode.getNext();
}
return currNode;
}
@Override
public int remove(int ind) {
if (ind < 0 || ind >= size) {
return -1; // LinkedList index overshoot
}
Node prev = null;
Node current = head;
for (int i = 0; i < ind; i++) {
prev = current;
current = current.getNext();
}
if (prev == null) {
head = current.getNext();
} else {
prev.setNext(current.getNext());
}
if (current == tail) {
tail = prev;
}
size--;
return ind; // Returns the index of the deleted node
}
@Override
public int remove(Node node) {
Node prevNode = null;
Node currNode = head;
int index = 0;
// Record the index of the current node
while (null != currNode && !currNode.equals(node)) {
prevNode = currNode;
currNode = currNode.getNext();
index++; // Increments the index on each loop
}
// node not found!
if (null == currNode)
return -1;
if (null == prevNode) {
// node is the list head
head = currNode.getNext();
} else {
// node is not the list head
prevNode.setNext(currNode.getNext());
}
// update the tail node if necessary
if (tail == currNode) tail = prevNode;
// Clear Node references to prevent memory leaks
size--;
// Returns the index of the removed node
return index;
}
@Override
public Node contains(int value) {
Node currNode = head;
while (currNode != null) {
if (currNode.getData() == value) {
return currNode;
}
currNode = currNode.getNext();
}
return null;
}
@Override
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
public Node first() {
return head;
}
public Node last() {
return tail;
}
public void clear() {
head = null;
tail = null;
size = 0;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
if (null == head) {
sb.append(": EMPTY_LIST");
} else {
sb.append(" (").append(size).append("): ");
Node currentNode = head;
int currLoc = 0;
while (null != currentNode) {
if (!sb.isEmpty()) sb.append("\n");
sb.append(String.format(" [%2d] ", (currLoc++))).append(currentNode.toString());
currentNode = currentNode.getNext();
}
}
return sb.toString();
}
}
<H2>Lecture 15 Stack and Queue</H2>
<H3>Memory model</H3>
<H3>Stack</H3>
An abstract data type (ADT)
A stack is a list in which insertion (push) and deletion (pop) take place at the same end.
Also known as last-in, first-out (LIFO) lists
栈是一种特殊的线性数据结构,它遵循后入先出(LIFO,Last In First Out)的原则。这意味着最后一个被放入栈中的元素将是第一个被取出的元素。这种特性使得栈在多种应用中非常有用,比如函数调用栈、撤销操作等。
▶ The last element will be the first to be retrieved from the list A pointer, top, is used to point to the top element of the stack
栈的实现通常使用一个指针(在编程中称为“栈顶指针”或“top”)来指向栈的顶部元素。这个指针可以是一个索引(在数组实现中)或是指向最后一个元素的引用/指针(在链表实现中)
Desirable stack operations; 理想的堆栈操作;
1.push(入栈):将一个新元素添加到栈的顶部。
2.pop(出栈):从栈的顶部移除一个元素,并返回这个元素的值。如果栈为空,则此操作可能会导致错误。
3.peek 或 top:查看栈顶元素的值,但不移除它。
堆栈的大小
- isEmpty:检查栈是否为空
- isFull: 堆栈是否为满(如果堆栈有界)
虽然栈和列表(List)都是线性数据结构,但它们的主要区别在于操作的方式和原则。列表允许在其任意位置进行插入和删除操作,而栈则只允许在其一端(称为“栈顶”)进行这些操作。
栈的应用
1.函数调用栈:在程序执行时,函数调用会形成一个栈,每个函数调用的参数和局部变量都被压入栈中,当函数返回时,它们会从栈中弹出。
2.撤销操作:在编辑文本或图形时,可以使用栈来保存用户的操作,以便用户能够撤销最近的更改。
3.括号匹配:在处理包含括号的表达式时,可以使用栈来检查括号的匹配性。
<H3>Stack code</H3>
这里有一个Stack
接口的声明,该接口定义了一系列与栈相关的操作。以下是一个基于这个接口的简单栈实现,它使用了Java的泛型来允许存储任何类型的对象。
首先,需要定义一个Node
类,尽管在许多栈的实现中并不直接使用节点类,因为可以直接使用数组或链表来存储元素。但为了与接口保持一致,假设有一个Node
类,它至少包含一个用于存储数据的字段。
java
// 假设的Node类
public class Node<T> {
private T data;
public Node(T data) {
this.data = data;
}
public T getData() {
return data;
}
// 如果需要,可以添加更多方法或字段
}
接下来是栈的实现:
Java
import java.util.ArrayList;
import java.util.List;
public class StackImplementation<T> implements Stack<Node<T>> {
private List<Node<T>> elements;
private int maxSize; // 如果需要限制栈的大小,可以设置这个值
public StackImplementation(int maxSize) {
this.elements = new ArrayList<>();
this.maxSize = maxSize;
}
public StackImplementation() {
this(Integer.MAX_VALUE); // 默认不限制栈大小
}
@Override
public int push(Node<T> node) {
if (isFull()) {
throw new IllegalStateException("Stack is full");
}
elements.add(node);
return elements.size(); // 返回当前栈的大小
}
@Override
public Node<T> pop() {
if (isEmpty()) {
throw new IllegalStateException("Stack is empty");
}
return elements.remove(elements.size() - 1); // 移除并返回栈顶元素
}
@Override
public Node<T> peek() {
if (isEmpty()) {
throw new IllegalStateException("Stack is empty");
}
return elements.get(elements.size() - 1); // 返回栈顶元素但不移除
}
@Override
public boolean isEmpty() {
return elements.isEmpty();
}
@Override
public boolean isFull() {
return maxSize != Integer.MAX_VALUE && elements.size() == maxSize;
}
@Override
public int size() {
return elements.size();
}
@Override
public boolean contains(Node<T> node) {
return elements.contains(node);
}
@Override
public void clear() {
elements.clear();
}
}
在这个实现中,我们使用了ArrayList
来存储栈中的元素。栈的大小可以通过构造函数的参数来限制(如果需要的话)。每个方法都遵循了Stack
接口中定义的行为。
注意,这里的push
方法返回了栈的大小,这在某些实现中可能不是必需的,但根据接口声明,我们实现了它。同样,如果栈为空或已满,pop
、peek
和push
方法会抛出IllegalStateException
异常。
在实际应用中,可能需要根据具体需求对异常处理、性能优化以及内存管理等方面进行调整。此外,如果不需要使用Node
类来封装数据,可以直接将StackImplementation
中的泛型类型从Node<T>
更改为T
,并在实现中直接使用泛型类型。
<H3>Stack Applications</H3>
<H4>左右括号匹配应用</H4>
使用栈(Stack)数据结构来检查符号匹配的应用。具体来说,它涉及到检查数学表达式中的左右括号(小括号、大括号、方括号)以及HTML和XML标签的匹配性。
1.创建一个空栈
2.读取下一个标记直到文件结束
读取输入文件(可能是数学表达式或HTML/XML代码)中的每一个字符或标记(token)。
3.1处理开放符号
3.1.1(例如“(”、“{”或“[”),我们将其压入栈中。
3.2处理关闭符号
3.2.1首先,检查栈是否为空。如果栈为空,说明没有匹配的开放符号,应该报告一个错误。
3.2.2如果栈不为空,从栈顶弹出一个元素。检查弹出的元素是否与当前关闭符号匹配。如果不匹配,也应该报告一个错误。
4.检查栈是否为空
如果不全空则报错
举例
以数学表达式为例:(a + b) * {c - [d * e]}
读取“(”,压入栈。
读取“)”,从栈中弹出“(”,匹配成功。
读取“[”,压入栈。
读取“]”,从栈中弹出“[”,匹配成功。
读取“{”,压入栈。
读取“}”,从栈中弹出“{”,匹配成功。
<H4>运算应用</H4>
Postfix expression evaluation
Infix form : 4 + ((5 × 6) / 3)
Postfix form: 4 5 6 × 3 / +
Read in the next token from the expression
If the token is an integer, push its value onto the stack;
Otherwise // the token is an operator
(i) Pop the top two elements out of stack
(ii) Apply the operator to the values
(iii) Push the result onto stack
Pop the top value out of stack
· 读取表达式中的下一个标记(token)。
· 如果标记是一个整数,将其值压入栈中。
· 否则,标记是一个运算符:
- 从栈中弹出顶部两个元素。
- 将运算符应用于这两个元素(即执行运算)。
- 将运算结果压入栈中。
· 最后,从栈中弹出顶部的值,这就是整个后缀表达式的计算结果。
以提供的后缀表达式 4 5 6 × 3 / +
为例,我们可以按照以下步骤来求值:
- 读取
4
,压入栈:[4]
- 读取
5
,压入栈:[4, 5]
- 读取
6
,压入栈:[4, 5, 6]
- 读取
×
,弹出6
和5
,计算5 × 6 = 30
,压入栈:[4, 30]
- 读取
3
,压入栈:[4, 30, 3]
- 读取
/
,弹出30
和3
,计算30 / 3 = 10
,压入栈:[4, 10]
- 读取
+
,弹出10
和4
,计算4 + 10 = 14
,压入栈:[14]
- 此时栈中只剩下一个元素,即整个表达式的计算结果。从栈中弹出
14
。
所以,后缀表达式 4 5 6 × 3 / +
的求值结果为 14
。
<H3>Stack Implementation</H3>
数组
▶静态:堆栈的大小最初给
▶设置顶部=0,和增加(减少)当新元素添加到(删除)堆栈
▶总是需要检查堆栈的大小来防止数组溢出
链表
▶动态:堆栈的大小没有界
▶总是添加/删除元素/从头
在两个实现,操作可以完成在恒数时间
▶数组实现,
⋆操作是在非常快的恒数时间
⋆可能不得不调整大小数组时,它是满的!
<H3>Queue</H3>
队列(Queue)也是一种抽象数据类型。
队列是以下列表:
▶插入(排队,或提供)已经完成
一端,然后
▶删除(退出队列,或删除)为
在另一端执行
与stack不同 LIFO last in first out
FIFO first in first out
<H3>Desirable operations 理想的操作</H3>
Insert a node in queue (i.e., queue up) 在队列中插入节点(即队列)
Remove a node from queue 从队列中删除一个节点
Get the first node in queue 获取队列中的第一个节点
Get size of the queue (i.e., queue length) 获取队列的大小(即,队列的长度)
Whether queue is empty 队列是否为空
Whether queue is full (if queue is bounded) 队列是否已满(如果队列有限制)
Queue: Implementation – Interface
public interface Queue {
int insert(Node node);
Node remove();
Node peek();
boolean isEmpty();
boolean isFull();
int size();
boolean contains(Node node);
void clear();
}
以上接口定义了一个队列的基本操作。队列是一种先进先出(FIFO)的数据结构,它允许我们在队列的尾部添加元素,并从队列的头部移除元素。以下是对该接口中每个方法的简单描述和可能的实现思路(以数组或链表为基础)。
首先,我们需要定义Node
类,因为接口中使用了Node
类型作为参数和返回值。假设我们有一个简单的Node
类,它可能看起来像这样:
Java
public class Node {
int data;
Node next;
public Node(int data) {
this.data = data;
this.next = null;
}
// 可能还有其他的getter和setter方法
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
Java
// 假设我们使用链表来实现队列
public class LinkedListQueue implements Queue {
private Node front; // 队列头部
private Node rear; // 队列尾部
private int size; // 队列当前大小
private int capacity; // 队列的最大容量(可选,如果队列有固定大小)
// 构造函数,可以指定队列的最大容量(如果队列有固定大小)
public LinkedListQueue(int capacity) {
this.front = null;
this.rear = null;
this.size = 0;
this.capacity = capacity;
}
// 或者无参构造函数,表示队列没有固定大小
public LinkedListQueue() {
this(-1); // -1 表示没有固定大小
}
@Override
public int insert(Node node) {
// 检查队列是否已满
if (isFull()) {
return -1; // 或者抛出异常
}
// 将新节点添加到队列尾部
if (rear == null) {
front = rear = node;
} else {
rear.next = node;
rear = node;
}
size++;
return size; // 返回插入后的队列大小
}
@Override
public Node remove() {
// 检查队列是否为空
if (isEmpty()) {
return null; // 或者抛出异常
}
// 移除队列头部的节点
Node temp = front;
front = front.next;
// 如果队列变成空的,将rear也设为null
if (front == null) {
rear = null;
}
size--;
return temp; // 返回被移除的节点
}
@Override
public Node peek() {
// 返回队列头部的节点,但不移除它
return front;
}
@Override
public boolean isEmpty() {
// 检查队列是否为空
return front == null;
}
@Override
public boolean isFull() {
// 检查队列是否已满
return capacity > 0 && size == capacity;
}
@Override
public int size() {
// 返回队列的大小
return size;
}
@Override
public boolean contains(Node node) {
// 检查队列是否包含指定的节点
Node current = front;
while (current != null) {
if (current.data == node.data) {
return true;
}
current = current.next;
}
return false;
}
@Override
public void clear() {
// 清空队列
front = null;
rear = null;
size = 0;
}
}
注意:
1.isFull()
方法仅在队列有固定大小时有意义。如果队列大小不固定, 可以不实现这个方法或始终返回false
。
2.contains()
方法通过遍历队列来查找节点,这里假设Node
类中的data
字段用于比节点是否相同。在实际应用中,可能需要更复杂的比较逻辑。
3.在这个实现中,我们使用了单链表作为队列的基础数据结构。也可以使用数组或其他数据结构来实现队列。
<H3>Queue Applications </H3>
队列的应用非常广泛,从日常生活中的排队到复杂的计算机系统操作,都可以看到队列的身影。下面,我将针对你提到的几个应用场景进行详细讲解。
一、顾客或服务排队
▶ Customers queue up at check out counter in supermarket for service
▶ Print spooling queue
在打印任务中,当多个文档或图片需要同时打印时,打印机会按照请求的顺序将它们放入打印队列中。这样,即使打印机一次只能处理一个任务,也能确保所有任务都能得到有序的处理。如果某个任务因为某种原因无法打印,我们可以将其从队列中移除或重新提交,而不会影响其他任务的执行。
▶ I/O event queue
在操作系统或网络编程中,I/O事件队列被用于处理输入/输出操作。当系统接收到一个I/O请求(如读取文件、接收网络数据等)时,它会将这个请求放入I/O事件队列中。然后,系统会按照队列中的顺序来处理这些请求。这种方式可以有效地控制请求的处理顺序,避免因为请求过多而导致的系统崩溃或性能下降。
二、交通模拟
Buses and private cars queue up at road junction, waiting to enter the highway
三、网络流量与有界缓冲区
Network messages (like emails) are routing through computers in the
network. Each computer allocates fixed amount of memory (i.e.,
buffer) to hold the messages in transition
<H3>Queue: implementation</H3>
与堆栈类似,队列可以使用以下方式来实现:
Similar to stack, a queue can be implemented using:
Linked list
▶ Dynamic: size of the queue is not bounded
▶动态:队列的大小没有限制
▶ Always enqueue at the rear and dequeue at the front
▶总是在后面排队,在前面不排队
(You can re-use some of the code in the linked list!)
Array
▶ Static: size of the stack is given initially
▶ However, how to implement the enqueue and dequeue operations are a bit tricky
▶静态:最初给出了堆栈的大小
▶但是,如何实现排队和出队列操作有点棘手
Consider the scenario below.
When enqueuing, the pointer front is fixed at index 0 while the
pointer rear moves forward in the array
When dequeuing, element at the front of the queue is removed.
▶ Therefore, all the elements after it need to be moved by one position.
▶ Results in O(n) running time for the dequeue method
To resolve the issue,
▶ A better way:
⋆ When enqueue, the pointer rear moves forward by one element
⋆ When dequeue, the pointer front also moves forward by one element
However, the problem here is that the pointer rear cannot move
beyond the last element in the array
We can improve the situation by using a circular array
However, there remain a challenge with the revised approach:
⋆ How to detect an empty or full queue?
⇒ Use a counter to count the number of elements in the queue.
boolean isFull() {
return (avail+1) % max_size == front ;
}
Operation | Stack | Queue |
add (insert or push) | O(1) | O(1) |
remove (remove or pop) | O(1) | O(1) |
peek | O(1) | O(1) |
contains | O(n) | O(n) |
size | O(1) | O(1) |
isEmpty | O(1) | O(1) |
isFull | O(1) | O(1) |
<H3>Questions</H3>
- 是否可以使用栈(Stack)数据结构(带有push和pop操作)的实例来实现队列(Queue)的功能,包括插入(enqueue)和移除(dequeue)操作?
对于第一个问题,确实可以使用两个栈来实现队列的功能。基本思路是,使用两个栈,一个用于输入(enqueue),另一个用于输出(dequeue)。当进行enqueue操作时,直接将元素压入输入栈;当进行dequeue操作时,如果输出栈为空,则将输入栈中的所有元素逐个弹出并压入输出栈,然后再从输出栈中弹出栈顶元素。这样可以保证元素的入队和出队顺序遵循FIFO(先进先出)原则。
2.否可以使用队列(Queue)数据结构(带有插入和移除操作)的实例来实现栈(Stack)的功能,包括push和pop操作?
对于第二个问题,使用一个队列实现栈的功能稍微复杂一些。基本的思路是,使用队列来模拟栈的push和pop操作。当进行push操作时,将新元素加入队列的末尾;当进行pop操作时,需要先将队列中除了最后一个元素以外的所有元素逐个出队并重新入队,直到队列中只剩下一个元素,然后将这个元素出队。这样可以模拟栈的后进先出(LIFO)特性。
<H2>Lecture 16 Trees </H2>
与链表类似,树T是递归式的节点集合,
如果不是空的,树T由
▶a(区分)节点r,即根,和
▶0或更多的非空子树T1,...,Tn(不同大小)组成
树是一种重要的数据结构,用于模拟具有树状结构性质的数据集合。树由节点(或顶点)和边(或链接)组成,其中每个节点可能包含数据,并且可能与其他节点通过边相连。在树中,除了根节点外,每个节点都有一个父节点,而一个节点可以有零个或多个子节点。
树结构在算法中有很多应用,包括但不限于搜索(如二叉搜索树)、排序(如堆排序)、数据压缩(如霍夫曼编码)和图形表示(如决策树、解析树)。
<H3>Tree相关定义及解释</H3>
Root
在树的数据结构中,最上面的节点被称为“根节点”(root node)或简称为“根”(root)。根节点是树的起始点,它位于树的最顶层,没有父节点,但可能有零个或多个子节点。这些子节点又可以各自拥有它们自己的子节点,从而形成了一个层次化的结构。
Parent and child (父子节点)
1.每个节点除了root节点, 都有Parent节点
2.一个节点,包括root节点,可以有零个或多个child 节点
edge(边)
表示的是节点与节点之间的逻辑连接
Leaves (or leaf nodes)(叶片节点)
叶节点没有子节点
Sibling (兄弟姐妹之意)
具有相同parent节点
Degree of a node 节点的度
是它所具有的子节点的数量。一棵树的度是的其元素度的最大值。
Path 路径
从一个节点到另一个节点的一条边序列
Depth of a node 节点的深度
▶ number of edges from the root to that node, e.g., depth(10) = 2
从根节点到此节点的边数
Height of a tree 树的高度
▶ the longest path from the root to a leaf node, e.g., height(tree) = 3
从根到叶节点的最长路径
Ancestor and descendant 祖先(Ancestor)和后代(Descendant)
An ancestor of a node is any node on the path from the node to the root,
e.g., ancestor(9) = {1, 3}
▶ A descendant is the inverse relation of ancestor, i.e., a node p is a descendant of a
node q if and only if q is an ancestor of p,
e.g., descendant(3) = {8, 9, 15},
descendant(4) = {10}
就是所有的子节点和所有的父节点
示例:(简化的)Unix目录结构
<H3>Example: Expression trees 表达式树</H3>
<H3>Binary tree 二叉树</H3>
二叉树(Binary Tree)以树形结构展示数据元素之间的层次关系,每个节点最多有两个子节点,通常被称为左子节点和右子节点。这种结构不仅简洁明了,
而且在很多实际问题中都能找到应用,如索引实现、语法树构建、决策树和搜索树等。
<H4>二叉树要么为空要么非空</H4>
空二叉树就是没有任何节点的二叉树,它相对简单,没有太多的操作空间。而非空二叉树则至少包含一个节点,我们称之为根节点。这个根节点指向两个不相交的子二叉树,即左子树和右子树。这种递归的定义方式使得二叉树可以无限扩展,形成复杂的树状结构。
二叉树由一个名为root的节点组成
▶Root指向两个不相交的二叉树子树:左右两个子树
<H4>满二叉树</H4>
满二叉树是一种所有节点要么没有子节点(度为0),要么有两个子节点(度为2)的二叉树,并且所有度为0的节点都在同一层上。而完全二叉树则是深度为k的、有n个节点的二叉树,它的特点是每一个节点都与深度为k的满二叉树中编号从1到n的节点一一对应。
<H4>完全二叉树</H4>
完全二叉树(Complete Binary Tree)是一种特殊的二叉树结构,它满足以下两个条件
层序遍历特性:完全二叉树从根节点开始,按层序遍历(从上到下、从左到右)时,除了最后一层外,其他层的节点数都是满的。也就是说,每一层的节点都紧密排列,没有间隔。
最后一层的特性:在完全二叉树的最后一层(也就是最底层),节点尽可能地靠左排列。换句话说,如果完全二叉树的深度为h,那么除第h层外,其它各层的节点数都达到最大个数,第h层有节点,并且节点只出现在该层最左边的若干位置上。
<H4>完美二叉树</H4>
完美二叉树,是一种特殊的完全二叉树。它的每一层都被完全填充,没有任何空位,形状像一个稳定的三角形。换句话说,从根节点到任意叶子节点的每一层都是满的,且所有叶子节点都位于同一层。此外,完美二叉树的每个内部节点(非叶子节点)都有两个子节点,因此其树度(即节点的子节点数)为2。
<H4>二叉树的高度 </H4>
二叉树的高度指的是从根节点到最远叶子节点的最长路径上的边数。二叉树中最长路径的长度。
一个高度为k的二叉树至少包含k+1个节点(当二叉树为线性链时),
最多包含2^(k+1) - 1个节点(当二叉树为完美二叉树时)。
<H4>二叉树遍历Tree traversal (cont.)</H4>
遍历二叉树是按照某种顺序打印或搜索树中数据的方法。有三种基本的遍历方式:前序遍历、中序遍历和后序遍历。
+
/ \
- X
/ \ / \
a X + g
/\ /\
b c / f
/\
d e
<H5>前序遍历(Preorder Traversal)</H5>
前序遍历首先访问根节点,然后遍历左子树,最后遍历右子树。
Prefix expression:
+ - a X b c X + / d e f g
<H5>中序遍历(Inorder Traversal)</H5>
中序遍历首先遍历左子树,然后访问根节点,最后遍历右子树。中序遍历常用于二叉搜索树,因为这样可以得到有序的元素序列。
Prefix expression:
a b c × - d e / f + g × +
<H5>后序遍历(Postorder Traversal)</H5>
后序遍历首先遍历左子树,然后遍历右子树,最后访问根节点。
Prefix expression:
a - b × c + d / e + f × g
<H4>二叉树搜索的缺点</H4>
1.Elements need to be sorted first 1.元素需要首先进行排序
2.Requires a sequential storage 2.需要顺序存储
3.Not appropriate for linked lists (Why?) 3.不用于链表
- 如果应用场景主要关注于按照元素的插入顺序进行遍历(如FIFO或LIFO),那么链表可能更合适,因为链表可以直接通过指针或引用进行遍历。而在二叉搜索树中,遍历可能需要额外的操作来维护树的平衡或保证搜索效率。
- 如果数据集合很大且无法全部加载到内存中(即外部排序或大数据处理场景),链表可能更易于处理,因为链表可以逐个加载和处理元素,而不需要一次性加载整个树结构。
- 在某些特定的硬件或内存限制条件下,链表可能由于其简单的结构和较小的内存占用而更受欢迎
<H4>Binary Search Tree (BST)二叉搜索树</H4>
Binary Search Tree (BST) 是一种特殊的二叉树,其中每个节点包含一个键值以及两个指向其子节点的链接(如果存在的话)。BST 的主要特点是它保持了节点的键值在树中的有序性,使得搜索、插入和删除操作更加高效。以下是 BST 的详细讲解:
<H5>1. 定义和性质</H5>
- 二叉树结构:BST 是一种二叉树,即每个节点最多有两个子节点,通常称为左子节点和右子节点。
- 有序性:BST 的主要特性是其节点键值的有序性。对于树中的任何节点
N
,其左子树中的所有节点键值都小于N
的键值,而其右子树中的所有节点键值都大于N
的键值。 - 唯一性:BST 中的所有键值是唯一的,即没有两个节点具有相同的键值。
<H5>2. 性质说明</H5>
- 根节点:BST 的根节点包含一个键值,并且其左子树和右子树(如果存在的话)也都是 BST。
- 左子树:对于根节点的任何左子树中的节点
y
,key(y) < key(root)
。 - 右子树:对于根节点的任何右子树中的节点
z
,key(z) > key(root)
。 - 递归性质:BST 的左子树和右子树也都是 BST,这意味着它们各自满足 BST 的所有性质。
<H5>3. 操作</H5>
BST 支持以下主要操作,并且这些操作的时间复杂度在平均情况下是 O(log n),其中 n 是树中节点的数量:
- 搜索:给定一个键值,BST 可以高效地查找树中是否存在具有该键值的节点。搜索过程从根节点开始,并根据键值的大小选择向左子树或右子树递归搜索。
- 插入:向 BST 插入一个新键值涉及遍历树以找到新节点的正确位置(即,小于当前节点的键值向左,大于当前节点的键值向右),然后创建一个新节点并链接到树中。
- 删除:从 BST 中删除一个节点涉及几个步骤,包括找到要删除的节点、处理该节点的子节点(如果有的话),以及重新平衡树(如果需要)。
<H5>4. 平衡问题</H5>
虽然 BST 提供了有效的搜索、插入和删除操作,但在某些情况下,树可能会变得不平衡,导致操作效率下降。为了解决这个问题,人们开发了几种自平衡的二叉搜索树,如 AVL 树、红黑树和 B 树/B+ 树,它们通过保持树的平衡来确保操作的高效性。
<H5>5. BST示例</H5>
一个简单的 BST 示例可能如下所示(数字表示节点的键值):
5
/ \
3 7
/ \ / \
2 4 6 8
在这个示例中,每个节点的左子树中的键值都小于该节点的键值,而右子树中的键值都大于该节点的键值。
<H4>二叉树结构中的元素集合</H4>
以某种方式将键存储在二叉树的节点中,这样就可以有效地进行搜索、插入和删除。
所有的键都是唯一的!也就是说,没有两个元素具有相同的键
根的左侧子树中的键(如果有)要小于根中的键
根的右子树中的键(如果有)大于根中的键
根的左右子树也是二元搜索树(BSTs)
BST的顺序遍历会按排序的顺序打印出所有的键
Inorder expression: 11, 15, 18, 19, 20, 22, 30, 37, 49, 50, 65
1 Compare 30:19 (the root), go to right
subtree;
2 Compare 30:50, go to left subtree;
3 Compare 30:22, go to right subtree;
4 Compare 30:37, go to left subtree;
5 Compare 30:30 found it!
⇒ 时间复杂度: O(Height of the tree)
19
/ \
5 50
/ / \
16 22 65
/ \
20 37
/ \
30 49
<H4>例子:Find Min and Max</H4>
FIND-MIN(x)
// x: root of a subtree
while LEFT(x) = NIL
x = x.left
return x
FIND-MAX(x)
// x: root of a subtree
while RIGHT(x) = NIL
x = x.right
return x
最小找最左,最大找最右
<H2>Lecture 17 Trees Insertion and Deletion</H2>
<H3>Trees Insertion</H3>
从树顶开始,大于往右小于往左
- x = T.root:从树的根节点开始查找插入位置。
- y = NULL:初始化 y 为空,它将用于跟踪 z 的父节点。
- while x != NULL:只要当前节点 x 不是空(即我们还没有到达叶子节点),就继续循环。
- y = x:将 x 的值赋给 y,以便在循环的下一次迭代中,y 将指向 z 的潜在父节点。
5-8. 判断 z 的键值与 x 的键值的大小关系,并决定向左还是向右移动。 - if y == NULL:如果 y 是空的,说明树 T 原本是空的,因此 z 应该成为新的根节点。
- T.root = z:将 z 设置为树的根节点。
11-14. 如果 y 不是空的,那么 z 应该成为 y 的左子节点或右子节点,具体取决于 z 的键值与 y 的键值的大小关系。
<H3>Trees Deletion</H3>
删除的案例
<H3>Binary search tree (BST) </H3>
我们该采取一个怎么样的方案来处理删除节点的树
删除节点后他的子节点该怎么处理?
必须使二叉搜索树的属性 (BST)维持。
<H4>Successor </H4>
我们可以去先确认每个节点的后继节点。
后继节点的可能有右边的子节点也可能没有子节点
后继节点中序遍历当前节点的下一位。
节点 | Successor |
2 | 3 |
3 | 4 |
4 | 6 |
6 | 7 |
7 | 9 |
9 | 13 |
13 | 15 |
15 | 17 |
17 | 18 |
18 | 20 |
20 | Null |
<H4>Three Case </H4>
情形一:无子节点 直接删除 “他自杀了”
情形二:有右边子节点无左边子节点 右边子节点代替了父节点
情形三:如果有左右节点,则选择右边子节点的最左边最低一级的子节点。
这就是中序遍历的规则。
总结上述情况
Three cases:
▶ If z has no children, then simply remove it by modifying its parent to replace z with null as its child
▶ If z has just one child, then elevate that child to take z’s position in
the tree by modifying z’s parent to replace z’s child
▶ If z has two children
<H4>时间复杂度 </H4>
<H4>Problems with BSTs</H4>
How can we predict the height of the tree?
Many trees of different shapes can be composed of the same data
How to control the tree shape?
树的形状和高度都难以控制
Problem of Lopsidedness
1.不平衡2.子节点不都是两个
极端情况,全是右子节点,变成链表了
<H4>Balanced vs Unbalanced Tree</H4>
<H4>How Fast is Sorting in BST? </H4>
n 个元素(n 个很大)首先构造一个 BST,然后按顺序读取它们
越平衡越快,越不平衡越慢。
Bad case: the input is more or less sorted
▶ A rather linear tree is constructed
▶ Total steps in constructing a BST: 1 + 2 + · · · + n = n(n + 1) /2 ∼ n^2
▶ Total steps in traversing the tree: n
⇒ Total: O(n^2)
Best case: the BST is constructed in a balanced manner
▶ Depth after adding i numbers: lg i
▶ Total steps in constructing a BST:
lg 1 + lg 2 + · · · + lg n < lg n + lg n + · · · + lg n = n lg n
▶ Total steps in traversing the tree: n
⇒ Total: O(n lg n) – much faster
For any arbitrary input, one can indeed construct a rather balanced BST with some extra steps in insertion and deletion
<H2>Tutorial 02 Stack and Queue & tree</H2>
<H3>Stack and Queue</H3>
<H4>Problem 1 Stack&Queue 操作</H4>
What is the output of the following program?
Queue q = new Queue(); q[]
Stack s = new Stack(); s[] q[]
s.push(new Integer(5)); s[5] q[]
s.push(new Integer(6)); s[6,5] q[]
s.push(s.peek()); s[6,6,5] q[]
s.push(new Integer(7)); s[7,6,6,5] q[]
q.enqueue(s.pop()); s[6,6,5] q[7]
q.enqueue(new Integer(5));q[7,5] s[6,6,5]
q.enqueue(new Integer(6)); q[7,5,6] s[6,6,5]
System.out.println(q.peek()); println 7
s.push(q.dequeue()); q[5,6] s[7,6,6,5]
System.out.println(s.pop()); q[5,6],s[6,6,5] println 7 7
s.pop(); q[5,6],s[6,5]
System.out.println(s.pop()); q[5,6],s[5] println 7 7 6
A. 567
B. 765
C. 776
D. 766
E. None of the above
1.Initialization:
Queue q = new Queue();
Stack s = new Stack();
2.Pushing elements to the stack:
1.s.push(new Integer(5)); 堆栈现在包含:[5]
2.s.push(new Integer(6)); 堆栈现在包含:[6, 5]
3.s.push(s.peek());
s.peek()返回堆栈顶部的元素,即6s.push(6);
(实际上是s.push(new Integer(6));,但值相同,所以可以简化为6)
堆栈现在包含:[6, 6, 5]
s.push(new Integer(7)); 堆栈现在包含:[7, 6, 6, 5]
3. Enqueuing elements to the queue:
q.enqueue(s.pop());
q.enqueue(new Integer(5));
q.enqueue(new Integer(6));
First, s.pop() removes and returns the top element from s, which is 7. So, q now contains: [7].
首先, s.pop() 从 s 中删除并返回顶部元素,即 7 。因此,堆栈s现在包含:[ 6, 6, 5], q 现在包含: [7] 。然后, 5 和 6 排队到 q 。因此, q 现在包含: [7, 5, 6] 。
Then, 5 and 6 are enqueued to q. So, q now contains: [7, 5, 6].
4.Printing the first element of the queue:
System.out.println(q.peek());
This will print the first element of q, which is 7.
这将打印 q 的第一个元素,即 7 。
5.Pushing a dequeued element to the stack:
s.push(q.dequeue());
q.dequeue() removes and returns the first element from q, which is 7.
q.dequeue() 从 q 中删除并返回第一个元素,即 7 。
因此, 7 被推送到 s 。现在, s 包含: [7, 6, 6, 5] 。
q 包含: [7, 5, 6] 。
s 包含: [7, 6, 6, 5] 。
6.Popping elements from the stack: 从堆栈中弹出元素:
System.out.println(s.pop());
s.pop();
System.out.println(s.pop());
s.pop() removes and returns the top element from s, which is 7. This is printed.
The next s.pop() removes and does not print 6 (from the second position in s).
The last s.pop() removes and prints the new top element of s, which is 6.
s.pop() 从 s 中删除并返回顶部元素,即 7 。这是打印出来的。
下一个 s.pop() 删除并且不打印 6 (从 s 中的第二个位置)。
最后一个 s.pop() 删除并打印 s 的新顶部元素,即 6 。
Output
C 776
<H4>Problem 2 Linear time</H4>
Consider the implementation of the Queue using an array. What would go wrong if we try to keep all the items at the front of a partially-filled array (so that data[0] is always the front of the queue)?
A. The constructor would require linear time
B. The enqueue method would require linear time
C. The isEmpty method would require linear time
D. The dequeue method would require linear time
E. The peek method would require linear time
(D)
A. 构造函数会需要线性时间
构造函数通常只需要初始化数组和设置队列的状态(例如,头尾指针的位置)。这通常是常数时间操作,与数组的大小无关。所以A不正确。
B. enqueue方法会需要线性时间
在标准的队列实现中,enqueue操作是常数时间的,因为新元素可以直接添加到数组的末尾。但是,如果所有元素都保持在数组的前面,并且数组已满(没有空间在头部插入新元素),那么可能需要移动所有元素以腾出空间,这会导致线性时间复杂度。然而,问题描述中说的是“部分填充的数组”,所以通常情况下enqueue不需要线性时间。因此B不正确。
C. isEmpty方法会需要线性时间
isEmpty方法通常只需要检查队列的状态(例如,头部和尾部指针是否相等)来判断队列是否为空。这是一个常数时间操作。所以C不正确。
D. dequeue方法会需要线性时间
如果所有元素都保持在数组的前面,并且当从队列中删除元素(即执行dequeue操作)时,所有剩余的元素都需要向前移动以填补被删除元素的位置。这是一个线性时间操作,因为需要移动的元素数量与队列中剩余的元素数量成正比。因此D是正确的。
E. peek方法会需要线性时间
peek方法只是查看队列的头部元素而不删除它。这通常只需要返回头部指针所指向的元素,是一个常数时间操作。所以E不正确。
<H3>Tree</H3>
<H4>Problem 3 Min-heap</H4>
What is the different between the binary search tree (BST) property and the min-heap property on page 163 (i.e., A[PARENT(i)] ≤ A[i])? Can the min-heap property be used to print out the keys of an n-node tree in sorted order in O(n) time? Show how, or explain why not.
The BST property guarantees that all nodes in the left subtree are smaller, and respectively, and all nodes in the right subtree are larger, than the root. Whereas the min-heap property only guarantees that the child is larger than its parent, but does not distinguish between the relation between the left and right children. Therefore, the min-heap property cannot be used to print out the keys in sorted order in O(n) time as we have no way to distinguish which subtree contains the smallest element.
BST 属性保证左侧子树中的所有节点都比根小,并且右侧子树中的所有节点都大于根。而 min-heap 属性仅保证子项大于其父项,但不区分左子项和右子项之间的关系。因此,min-heap 属性不能用于在 O(n) 时间内按排序顺序打印出键,因为我们无法区分哪个子树包含最小的元素
为什么 min-heap 不能在 O(n) 时间内按排序顺序打印出键:
- 堆的性质:最小堆保证的是根节点是最小的,但它并不保证其他节点的相对顺序。这意味着我们不能简单地通过遍历堆的节点来得到一个排序的序列,因为这样的遍历可能不是有序的。
- 删除最小元素:如果我们从最小堆中连续删除最小元素(即根节点),并打印它们,我们可以得到一个排序的序列。但是,每次删除操作都可能需要重新排列堆(通常是“下滤”操作,即将最后一个元素移到根位置,然后向下调整以保持堆的性质),这个操作的时间复杂度是 O(log n)。因此,连续删除并打印 n 个元素的总时间复杂度是 O(n log n),而不是 O(n)。
- 构建有序序列:如果我们想要一个排序的序列,更好的方法是使用排序算法,如快速排序、归并排序、堆排序等。这些算法可以在 O(n log n) 或更好的时间复杂度内对数组或列表进行排序。
注意:虽然堆排序本身使用了堆数据结构,但它的时间复杂度是 O(n log n),而不是 O(n)。这是因为堆排序需要构建堆(O(n)),然后连续执行 n 次删除最小元素操作(每次操作都是 O(log n))。
<H4>Problem 4 BST</H4>
Professor Kilmer claims to have discovered a remarkable property of binary search tree (BST). Suppose that the search for key k in a BST ends up at a leaf. Consider three sets: A, the keys to the left of the search path; B, the keys on the search path; and C, the keys to the right of the search path. Professor Kilmer claims that any three keys a ∈ A, b ∈ B, and c ∈ C must satisfy a ≤ b ≤ c. Give a smallest counterexample to the professor’s claim.
A (keys to the left of the search path):
这个集合包含所有在搜索路径左侧(即值小于搜索键)的键。
B (keys on the search path):
B(搜索路径上的键):
这个集合包含搜索路径上的所有键,也就是实际被比较过的键。
C (keys to the right of the search path):
C(搜索路径右侧的键):
这个集合包含所有在搜索路径右侧(即值大于搜索键)的键。
<H4>Problem 5 BST</H4>
Suppose that we have numbers between 1 and 100 in a binary search tree (BST) and want to search for the number 45. Which (possibly multiple) of the following sequences could be the sequence of nodes examined?
(a) 5, 2, 1, 10, 39, 34, 77, 63
(b) 1, 2, 3, 4, 5, 6, 7, 8
(c) 9, 8, 63, 0, 4, 3, 2, 1
(d) 8, 7, 6, 5, 4, 3, 2, 1
(e) 50, 25, 26, 27, 40, 44, 42
(f) 50, 25, 26, 27, 40, 44
在二叉搜索树(BST)中,搜索一个节点的过程总是遵循以下规则:
- 从根节点开始。
- 如果要搜索的节点值小于当前节点值,则搜索左子树。
- 如果要搜索的节点值大于当前节点值,则搜索右子树。
- 如果找到相等的节点值,则搜索结束。
- 如果在子树中没有找到,则回溯到父节点并继续搜索。
(a) NOT possible. The first element in the sequence must be the root element. When we compare the root key 5 with the sought key 45, we see that 5 < 45, and so 45, if present in the tree, must be in the right subtree of the root. The second element in the sequence (2) should therefore be the root of the right subtree of the root node. But all keys in the right subtree are strictly greater than 5, and therefore 2 cannot be the key root of the right subtree.
(b) Possible. It is the sequence that is investigated in the degenerated tree consisting of elements {1, 2, 3, 4, 5, 6, 7, 8}, where no node has a left child.
(c) NOT possible. If we go to 8 from 9, then we are going in the wrong direction to find 45.
(d) NOT possible. Similar to the case above, it is not possible.
(e) NOT possible. All steps are in the right direction except the last one. From 44 we should check the
right child, which has a key greater than 44. Instead we go to 42.
(f) Possible.
<H4>Problem 6</H4>
Suppose you have a set of numbers where you over some period of time do a constant number of insertions and a linear number of lookups of the maximum. To maintain your set of numbers, you can choose between using either a heap or a binary search tree (BST).
假设您有一组数字,在一段时间内,您执行恒定数量的插入和最大值的线性查找次数。要维护您的数字集,您可以选择使用堆或二叉搜索树 (BST)。
(a)Assume you are interested in minimizing the average total runtime over the time period. For each structure, what is the asymptotic upper bound on the average case runtime, and which of the two structures should you choose?
假设您有兴趣最小化该时间段内的平均总运行时间。对于每个结构,平均案例运行时间的渐近上限是多少,您应该选择两种结构中的哪一种?
(b) Suppose you realize that you actually interested in the worst case. For each structure, what is the asymptotic upper bound on the worst case runtime, and which of the two structures should you chose now?
假设你意识到你实际上对最坏的情况感兴趣。对于每个结构,最坏情况运行时的渐近上限是多少,您现在应该选择这两种结构中的哪一种?
Each justification should be no longer than two sentences.
Answer
Let k be the number of insertions to be made. Let n be the number of elements in the set. By assumption there will be Θ(n) lookups of the maximum element.
设 k 为要插入的次数。设 n 为集合中的元素数。根据假设,将对最大元素进行 Θ(n) 查找。
(a)Insertion an element by bubbling a leaf up towards the root in a balanced tree is O(lg n). Insertion into a BST is done by traversing the tree from root to a leaf. In the average case, assuming that the tree to be somewhat balanced, the time consumption of traversal is proportional to the height of the tree, and so also O(lg n).
在平衡树中,通过向根部冒泡叶子来插入元素是 O(lg n)。插入 BST 是通过将树从根遍历到叶子来完成的。在平均情况下,假设树在某种程度上是平衡的,遍历的时间消耗与树的高度成正比,因此 O(lg n) 也成正比。
In order to look up the maximum element in a heap, we only have to look at the root element, which can be done in O(1) time. To lookup the maximum element in a BST, we have to find the rightmost node in the tree (FIND-MAX). In a somewhat balanced tree, the length of such traversal is likely to be close to the height of the tree. So, the time consumption is O(lg n) time.
为了查找堆中的最大元素,我们只需查看根元素,这可以在O(1)时间内完成。要查找BST中的最大元素,我们必须找到树中最右边的节点(FIND-MAX)。在某种程度上平衡的树中,这种遍历的长度可能接近树的高度。因此,时间消耗为O(lg n)时间
Hence, the total time consumption in the average case is k · O(lg n) + n · O(1) = O(n) when a heap is used; and k · O(lg n) + n · O(lg n) = O(n lg n) when a tree is used.
因此,当使用堆时,平均情况下的总时间消耗为k·O(lg n)+n·O(1)=O(n);
当使用树时,k·O(lg n)+n·O(lg n)=O(n lg n)。
Heap: O(n)
Tree: O(n lg n)
For heap, the time consumptions is dominated by a linear number of lookups, each performed in O(1) time. For the tree, the time consumptions is dominated by a linear number of lookups, each performed in O(lg n) time.
(b) For the heap, lookup is always done in constant time, and insertion is always a traversal from leaf to root in a balanced tree. So the time consumption remains bounded by O(n) even in the worst case. For the BST, we must consider the degenerate case when no node has a left child. Then, the tree
is effectively a list rooted in the smallest element, descending rightwards through ever increasing elements towards the greatest element. In the worst case, we have to insert elements that are even greater than the elements already present in the tree. Then we have to traverse the entire tree, going
through all O(n) nodes before reaching the correct place to insert the new element. Such an insertion runs in time bounded by O(n).
Lookup of the maximal element in such a degenerated tree is equally bad. Since the maximal element is kept in the rightmost node, in order to find it we have to traversal all O(n) nodes again.
This is done in O(n) time. So, the total time consumption is O(n) when a heap is used, and k · O(n) + n · O(n) = O(n^2) when
a BST is used.
Heap O(n)
Tree: O(n^2)
The heap behaves asymptotically the same in the worst case as in the average case. In a tree where no node has a left child, both insertion and lookup of maximal element is done in O(n) time.
对于堆,时间消耗主要由线性数量的查找主导,每个查找在 O(1) 时间内执行。对于树,时间消耗由线性数量的查找主导,每次查找以 O(lg n) 时间为单位执行。
(a) 在考虑平均总运行时间以最小化的情况下,我们来分析堆和二叉搜索树(BST)的渐近上界。
- 堆:
- 插入操作:对于堆来说,插入操作的时间复杂度是O(log n),因为每次插入元素后可能需要重新调整堆的结构以保持其性质。
- 查找最大值:在堆中查找最大值总是O(1)的复杂度,因为最大值总是在根节点。
因此,堆的平均情况渐近上界是O(log n)的插入和O(1)的查找最大值。
nO(log n)+ O(1)= O(nlg n)
无论最坏还是平均情况,都是O(nlg n)
- 二叉搜索树(BST):
- 插入操作:在BST中插入元素的时间复杂度是O(log n),但在最坏情况下可能退化为O(n)(当树变得不平衡时)。然而,在平均情况下,我们假设树是平衡的,所以插入操作的时间复杂度是O(log n)。
- 查找最大值:在BST中查找最大值需要遍历到最右边的叶子节点,其时间复杂度在最坏情况下是O(n),但在平均情况下也是O(log n)(如果树是平衡的)。
但是,为了查找最大值,BST通常不是最佳选择,因为需要遍历树直到最右侧。
平均情况是O(log n)+O(log n)=O(log n)
最坏情况是O(log n)+nO(n)= O(n^2)
结论:
- 如果是最小化平均总运行时间,并且需要频繁查找最大值,那么堆将是更好的选择,因为它提供了O(log n)的插入和O(1)的查找最大值操作。
<H2>Lecture 18-20 Hashtables</H2>
L18=L19-20
<H3>Hash table 定义</H3>
Also known as hash map, an abstract data type (ADT) that implements associative
array (of some fixed size).
Supports finds, insertions, and deletions of any named item.
Allows operations to be executed in constant average time (O(1))
A slot (or bucket) number is calculated by a hash function, h, that takes a variable-size input k, and return a fixed-size string (or int in [0, . . . , m − 1]), h(k), which is called the hash value of k, and m is the capacity of the table.
Usually, m << N
定义:
哈希表,也被称为哈希映射,它是一种实现关联的抽象数据类型(ADT)数组(具有固定大小)。
操作:
支持查找、插入和删除任何命名项。
时间复杂度:
允许以恒定的平均时间(O (1))执行操作
哈希表的用法:
一个槽(或桶)数是由一个哈希函数h计算的,它接受一个可变大小的输入k,并返回一个固定大小的字符串(或[0,…,m−1]中的int),h (k),这被称为k的哈希值,m是表的容量。
Tips:slot (or bucket):
用于存储数据元素的存储位置。当你通过哈希函数(hash function)将键(key)映射到一个整数时,这个整数通常用作索引来查找或存储数据。
哈希表的容量(m
)通常远小于可能要存储的键的总数(N
)
Hash table:
It provides a fast way to maintain a set of keys or map keys
to values, even when the keys are objects, like strings, while
“relationship” between elements are of less, or not,
important.
However. . .
Operations that requires any ordering information among
elements are not supported
findMin and findMax
Successor and predecessor
Report data within a given range
List out data in order
<H3>Hash table Applications</H3>
编译器中的符号表(Symbol Table in Compilers):
编译器在编译程序时,需要跟踪程序中声明的所有变量、函数、类型等。这些信息通常被存储在符号表中,以便在后续的编译过程中引用。符号表通常使用哈希表来实现,因为哈希表可以在常数平均时间内完成查找、插入和删除操作。这样,当编译器需要查找一个变量的类型或作用域时,它可以快速地在符号表中定位到该变量。
在线拼写检查器(On-line Spell Checkers):
在线拼写检查器在检查文档中的拼写错误时,也需要一个快速查找数据结构的支持。它们通常预先将整个词典进行哈希处理(即预哈希),并为词典中的每个单词生成一个哈希值。这样,当检查器需要验证文档中的某个单词是否拼写正确时,它只需计算该单词的哈希值,并在常数时间内查找哈希表中是否存在该哈希值。如果存在,说明单词拼写正确;如果不存在,则可能是拼写错误。这种方法允许拼写检查器以线性时间遍历文档中的每个单词,并实时打印出拼写错误的单词及其在原文档中的出现顺序。
<H3>Hashtables 冲突的处理</H3>
在处理哈希表中的冲突时,这是两种常见的策略:开放寻址法(Open Addressing)和链地址法(Separate Chaining,也称为拉链法)。
<H4>链地址法(Separate Chaining)<H4>
链地址法(Separate Chaining)是一种处理哈希冲突的方法,其核心思想是将具有相同哈希值的元素以链表的形式存储。具体来说,当两个或多个元素具有相同的哈希值时,它们被存储在同一个链表中,而不是在哈希表的每个槽位中。这种方法的主要优点是它能够有效地处理哈希冲突,而不需要预先知道会有多少冲突。
在这个例子中,我们将使用链地址法来处理冲突,因为这种方法实现起来相对简单,并且对于动态数据集合通常很有效。
首先,我们定义哈希函数 h(k) = (5k + 4) % 11 和一个初始为空的哈希表,其大小假设为11(因为我们的哈希函数对11取模)。
接下来,我们按照给定的顺序插入值:3, 9, 2, 1, 14, 6, 25。
插入值3:
计算 h(3) = (5 * 3 + 4) % 11 = 19 % 11 = 8
将3放入哈希表第8个槽位。由于这是第一个元素,所以没有冲突。
插入值9:
计算 h(9) = (5 * 9 + 4) % 11 = 49 % 11 = 5
将9放入哈希表第5个槽位。同样没有冲突。
插入值2:
计算 h(2) = (5 * 2 + 4) % 11 = 14 % 11 = 3
将2放入哈希表第3个槽位。没有冲突。
插入值1:
计算 h(1) = (5 * 1 + 4) % 11 = 9 % 11 = 9
将1放入哈希表第9个槽位。没有冲突。
插入值14:
计算 h(14) = (5 * 14 + 4) % 11 = 74 % 11 = 9
这里发生冲突了,因为值1已经位于第9个槽位。我们使用链地址法,在第9个槽位处添加一个链表,并将14放入该链表中。
插入值6:
计算 h(6) = (5 * 6 + 4) % 11 = 34 % 11 = 1
将6放入哈希表第1个槽位。没有冲突。
插入值25:
计算 h(25) = (5 * 25 + 4) % 11 = 129 % 11 = 1
这里再次发生冲突,因为值6已经位于第1个槽位。我们在第1个槽位的链表中添加值25。
在链地址法中,哈希表的每个槽位可以是一个链表(或其他动态集合),用于存储所有哈希到该槽位的键值对。当发生冲突时,我们只需将新的键值对添加到相应槽位的链表中即可。
最终,哈希表将包含以下元素(这里用伪代码表示):
Slot 0: [] (空)
Slot 1: [6 -> 25] (链表包含6和25)
Slot 2: [2]
Slot 3: [2]
Slot 4: [] (空)
Slot 5: [9]
Slot 6: [] (空)
Slot 7: [] (空)
Slot 8: [3]
Slot 9: [1 -> 14] (链表包含1和14)
Slot 10: [] (空)
这样就可以通过哈希函数和链地址法有效地处理哈希表中的冲突了。
<H4>开放寻址法(Open Addressing)</H4>
开放寻址法(Open Addressing)是在哈希表内部解决冲突的一种方法,它不会使用链表或其他数据结构来存储冲突的元素,而是使用哈希表中的其他空位来存储这些元素。
如果要插入的键k与现有的键k发生碰撞,则重新定位此键。
<H5>开放寻址法的思路</H5>
1.问题在于:
1.冲突的迁移方案
2.以后搜索冲突元素k的方法、
2.处理哈希冲突:
当两个或多个输入映射到相同的哈希值时,就需要一种策略来解决这种冲突。这通常是通过某种“探测”或“重新定位”机制来完成的。
3. 探测序列 (Probing Sequence):
当发生哈希冲突时,探测序列决定了我们如何在哈希表中查找下一个可能的槽位(slot)来存储该元素。
hi(k)=(h(k)+f(i))mod m, with f(0)=0
hi(k) 表示对于键 k 和第 i 次探测的哈希值。
h(k) 是基本的哈希函数,它将键 k 映射到一个初始的哈希值。
f(i) 是一个与探测次数 i 相关的函数,用于计算每次探测的偏移量。
m 是哈希表的大小(或称为模数)。
mod m 是取模运算,确保 hi(k) 落在哈希表的范围内。
4.f(i) 作为冲突解决策略或重新定位方案:
f(i)
的选择决定了如何处理哈希冲突。例如,在二次探测中,f(i) = i^2
(虽然你的描述中只提到了 f(0) = 0
,但二次探测通常使用 f(i) = i^2
)。这意味着当发生冲突时,我们会检查初始哈希值之后的 1^2
、2^2
、3^2
等位置,直到找到一个空的槽位。
使用不同的 f(i)
函数可以得到不同的探测序列,从而得到不同的冲突解决策略。
5.不同的策略
在开放寻址法中常用的方法包括:线性探测、二次探测和双哈希。
linear probing, quadratic probing, and double hashing
<H5>开放寻址的线性探测法(Linear probing)</H5>
给定的一个哈希函数是f (x) =(5x + 4)%11。
已知Slot如下:
Slot 0: [] Slot 1: [] Slot 2: [2] Slot 3: [] Slot 4: [] Slot 5: [] Slot 6: [] Slot 7: [] Slot 8: [] Slot 9: [] Slot 10: []
如果我们将值3、9、2、1、14、6和25插入到表中,现在,我们将按照给定的顺序插入值,并跟踪它们最终落在哈希表中的哪个位置。
假设哈希函数为 h(k) = (5k + 4) % 11,并且我们要插入的元素序列是 3, 9, 2, 1, 14, 6, 25。
哈希表初始为空,大小为11。
插入值3:
计算 h(3) = (5 * 3 + 4) % 11 = 19 % 11 = 8
将3放入哈希表第8个槽位。
插入值9:
计算 h(9) = (5 * 9 + 4) % 11 = 49 % 11 = 5
将9放入哈希表第5个槽位。
插入值2:
计算 h(2) = (5 * 2 + 4) % 11 = 14 % 11 = 3
将2放入哈希表第3个槽位。
插入值1:
计算 h(1) = (5 * 1 + 4) % 11 = 9 % 11 = 9
将1放入哈希表第9个槽位。
插入值14:
计算 h(14) = (5 * 14 + 4) % 11 = 74 % 11 = 9
冲突!位置9已经被1占据。使用线性探测,查找下一个空位。
检查位置 (9+1)%11 = 10,为空,所以将14放入第10个槽位。
插入值6:
计算 h(6) = (5 * 6 + 4) % 11 = 34 % 11 = 1
将6放入哈希表第1个槽位。
插入值25:
计算 h(25) = (5 * 25 + 4) % 11 = 129 % 11 = 1
冲突!位置1已经被6占据。使用线性探测,查找下一个空位。
检查位置 (1+1)%11 = 2,已经被2占据。
继续检查位置 (2+1)%11 = 3,也被占据。
继续检查位置 (3+1)%11 = 4,为空,所以将25放入第4个槽位。
使用线性探测的开放寻址法后,哈希表将如下所示:
Slot 0: [] (空) |
Slot 1: [6] |
Slot 2: [2] |
Slot 3: [2] |
Slot 4: [25] |
Slot 5: [9] |
Slot 6: [] (空) |
Slot 7: [] (空) |
Slot 8: [3] |
Slot 9: [1] |
Slot 10: [14] |
注意,在使用开放寻址法时,删除操作会变得复杂,因为你需要确保被删除的元素不会影响到后续的查找操作。通常,你可以通过标记已删除的元素(例如使用特殊值或“墓碑”标记)来处理这个问题。
<H5>开放寻址的二次探测法(Quadratic probing)</H5>
使用二次探测(Quadratic probing)的哈希表在发生哈希冲突时,会根据一个二次函数(在这里是 f(i) = i^2
)来计算下一个要检查的槽位。
例子:
给定的一对哈希函数是 h(k) = 5k + 4 和 f(i) = i^2,哈希表的大小是11,并且哈希函数的组合是 hi(k) = (5k + 4 + i^2) % 11。
已知Slot如下:
Slot 0: [] Slot 1: [] Slot 2: [2] Slot 3: [] Slot 4: [] Slot 5: [] Slot 6: [] Slot 7: [] Slot 8: [] Slot 9: [] Slot 10: []
如果我们将值3、9、2、1、14、6和25插入到表中,现在,我们将按照给定的顺序插入值,并跟踪它们最终落在哈希表中的哪个位置。
插入值 3:
h0(3) = (5*3 + 4) % 11 = 19 % 11 = 8
位置8是空的,所以值3被放在位置8。
插入值 9:
h0(9) = (5*9 + 4) % 11 = 49 % 11 = 5
位置5是空的,所以值9被放在位置5。
插入值 2:
h0(2) = (5*2 + 4) % 11 = 14 % 11 = 3
位置3是空的,所以值2被放在位置3。
插入值 1:
h0(1) = (5*1 + 4) % 11 = 9 % 11 = 9
位置9是空的,所以值1被放在位置9。
插入值 14:
h0(14) = (5*14 + 4) % 11 = 74 % 11 = 8(与3冲突)
使用二次探测,尝试下一个位置:
h1(14) = (h0(14) + 1^2) % 11 = (8 + 1) % 11 = 9(与1冲突)
h2(14) = (h0(14) + 2^2) % 11 = (8 + 4) % 11 = 2
位置2是空的,所以值14被放在位置2。
插入值 6:
h0(6) = (5*6 + 4) % 11 = 34 % 11 = 1
位置1是空的,所以值6被放在位置1。
插入值 25:
h0(25) = (5*25 + 4) % 11 = 129 % 11 = 8(与3冲突)
使用二次探测,尝试下一个位置:
h1(25) = (h0(25) + 1^2) % 11 = (8 + 1) % 11 = 9(与1冲突)
h2(25) = (h0(25) + 2^2) % 11 = (8+ 4) % 11 = 1(与6冲突)
h3(25) = (h0(25) + 3^2) % 11 = (8 + 9) % 11 = 6
位置6是空的,所以值25被放在位置6。
<H5>开放寻址的双哈希法(Double hashing)</H5>
双哈希(Double hashing)的主要思想是,当发生哈希冲突时,不是简单地使用固定的步长(如线性探测的1)或固定的数学公式(如二次探测的i^2)来寻找下一个空位,而是使用一个与键k
相关的步长。这样可以使得对于不同的键,其探测序列是不同的,从而减少聚集的可能性。
下面是对你给出的描述的详细解释:
基本公式
f(i) = i * h2(k)
:这里,i
是探测的步数(或称为探测的迭代次数),而h2(k)
是一个与原始哈希函数h(k)
不同的哈希函数。
hi(k) = (h(k) + i * h2(k)) mod N
:这是用于计算第i
次探测时的哈希地址的公式。其中,N
是哈希表的大小。
为什么h2(k)
需要与m
互质:
-
- 如果
h2(k)
与哈希表的大小m
不互质(即它们有公共的因子),那么h2(k)
可能只会使我们在哈希表的某些部分进行探测,而忽略其他部分。例如,如果h2(k)
是m
的因子,那么探测序列可能只会在哈希表的几个固定间隔的槽位上进行。 - 为了确保能够探测到哈希表中的所有槽位,我们需要确保
h2(k)
与m
互质。
- 如果
关于h2(k) = R − (k mod R)
:
-
- 选择一个比
m
小的素数R
,并定义h2(k)
如上,可以确保h2(k)
与m
互质的概率较高(当然,这并不是绝对的,但在实际应用中通常是一个合理的选择)。 - 这种方法的原理是,由于
R
是素数,并且k mod R
的结果是一个在[0, R-1]
范围内的整数,因此R - (k mod R)
将产生一个与R
互质的值(除非k
是R
的倍数,但这种情况下的冲突可以通过其他方式处理)。 - 然后,由于
R
小于m
并且是素数,所以R - (k mod R)
与m
互质的概率也相对较高(特别是当m
也是素数时)。
- 选择一个比
总之,双哈希通过引入一个与键k
相关的探测步长来减少聚集的可能性,并通过确保探测步长与哈希表大小互质来确保能够探测到哈希表中的所有槽位。
<H3>Hashtable Performance</H3>
哈希表的性能表现
首先,我们来分析一下哈希表(Hashtable)的性能,
讨论的是N/M。
其实就是负载因子(Load Factor)和元素分布的关系。
<H4>负载因子(Load Factor)</H4>
负载因子是哈希表中存储的元素数量(N)与哈希表的总槽位数(M)之间的比例,即 N/M。负载因子对于哈希表的性能至关重要,因为它直接影响哈希冲突的概率和查找、插入、删除操作的平均时间复杂度。
<H4>情况一:固定槽位数量(M),增加元素数量(N)</H4>
当哈希表的槽位数量(M)固定,而元素数量(N)不断增加时,负载因子 N/M
会逐渐增大。这会导致哈希冲突的概率增加,进而使得链表(或其他解决冲突的数据结构)的平均长度增加。如果链表平均长度接近 N/M
,则查找、插入和删除操作的平均时间复杂度将接近线性时间 O(N/M)
。
以您给出的例子为例,当 M = 5
,N = 19
时,平均链表长度 Q = N/M = 19/5 = 3.8
。当 N
变得非常大时,这个时间复杂度将接近 O(N)
,导致哈希表的性能急剧下降。
<H4>情况二:增加槽位数量(M),增加元素数量(N)</H4>
当哈希表的槽位数量(M)和元素数量(N)都增加,但保持 M = Θ(N)
(即 M
和 N
呈线性关系)时,负载因子 N/M
将保持常数,通常为一个小于 1 的值。这样,哈希冲突的概率会相对较低,链表(或其他解决冲突的数据结构)的平均长度也会保持较短。因此,查找、插入和删除操作的平均时间复杂度将接近常数时间 O(1)
。
然而,通过每次翻倍 M
的方式增加槽位数量可能会导致哈希表的空间占用过大。在实际应用中,我们通常会根据元素的增长情况和性能要求来动态调整 M
的大小,以保持一个较低的负载因子和较高的性能。
<H4>哪个哈希表性能更好?</H4>
从上述分析中可以看出,保持较低的负载因子(即 N/M
的值较小)对于哈希表的性能至关重要。因此,在第二种情况下(增加槽位数量 M
和元素数量 N
,并保持 M = Θ(N)
),哈希表的性能会更好,因为它能够保持较低的哈希冲突概率和较短的链表长度,从而实现接近常数时间的查找、插入和删除操作。
<H4>Time Complexity</H4>
O(1) in most operations
Why it appears most search mechanisms have performance at O(N) or O(log N)?
Why hash table can give us best performance?
Where does the magic come from?
<H3>Hashtable: Resize </H3>
哈希表(Hashtable)的扩容(Resize)操作
1.判断扩容时机:
当哈希表中元素的数量(N)与哈希表的总槽位数量(M)之间的比例,即负载因子 N/M,超过一个预设的阈值(如0.7或0.75)时,就需要进行扩容操作。这个阈值的选择通常基于哈希表的使用情况和性能要求。
2.分配更大的哈希表:
创建一个新的哈希表,其容量(即槽位数量)通常是原哈希表容量的两倍或更多。这样做是为了在保持较低负载因子的同时,减少哈希冲突的概率,提高哈希表的性能。
3.重新哈希(Rehash)表:
将原哈希表中的所有元素(键值对)重新计算哈希值,并根据新的哈希表容量确定它们在新表中的位置。这个过程通常涉及到遍历原哈希表,对每个元素执行哈希函数,并将结果对新的哈希表容量取模,得到新的索引位置。
将每个元素从原哈希表中取出,并插入到新的哈希表中对应的位置。如果新的位置已经存在其他元素(哈希冲突),则需要按照哈希表解决冲突的策略(如链地址法、开放寻址法等)进行处理。
4.删除较小的表(可选):
在完成所有元素的重新哈希和插入操作后,原哈希表就不再需要了,可以选择将其删除以释放内存空间。但请注意,在某些情况下(如多线程环境中),可能需要保留原哈希表直到所有对原哈希表的引用都被释放。
<H2>Lecture 21 Graphs</H2>
<H3>Graphs Applications</H3>
图形算法的复杂应用
<H3>Graphs Formal definitions</H3>
<H4>1.顶点集合V 边集合E与边的关系</H4>
A graph G is specified by an ordered pair (V, E), where V is the set of vertices and E is the set of edges
V是顶点的集合 E是边的集合
Neighbors 有边的端点
endpoints 边的两个端点
<H4>2.边的方向以及环</H4>
根据边的情况 分无向(双向)和单向
如果该对是无序的,即 {v1, v2} = {v2, v1},图是无向的;否则它是有向的。
it can be drawn as an arrow (called arc) (v1 , v2)
根据单向的边的指向,有边的循环则是Cyclic,无边的循环则是Acyclic。
<H4>3.边的序号和一个顶点的Deg</H4>
With edge labels: 边的序号
Degree of a vertex v, deg(v), is the number of edges incident to v.
Deg就是v的度,一共Deg(v)个边.
那么有一个边的情况下 会给两个顶点Deg各+1
<H4>4.Path路径</H4>
路径是顶点{v0,...,vn}的序列,使得{vi,vi+1},0≤i<n,是边。长度,n=路径上的边数例如,路径{a,f,e,d,c}是长度为4的路径循环是没有从节点回到自身的重复边的路径(如果有方向,则跟随箭头)(或者:路径是循环当且仅当v0=vn)例如,路径{a,b,c,a}是长度为3的循环a b c d e f图g路径循环a路径是简单的当且仅当它不包含相同的顶点两次.
Path就是依次连接边与边后,总共的边的数量
<H4>5.Connectivity连通性</H4>
A graph is connected if there is a (possibly directed) path between every
pair of distinct vertices i.e., if one vertex of the pair is reachable from the other
A directed acyclic graph (DAG) is a (rooted) tree iff it is connected, and every vertex but the root has exactly one parent
A connected, acyclic, undirected graph is also called a free tree, i.e., we are free to pick any node as the root
<H4>6.可能有顶点没路径连接到</H4>
“自由树”(free tree)=“连通、无环、无向图”,它实际上就是一个树(Tree)的特征
<H3>Edge</H3>
<H3>图的关系表示</H3>
<H4>矩阵表示</H4>
还可以用表格表示abcde之间是否有链接,当然无边的表格是对称的,单边是非对称。
有一种很罕见的矩阵。 入射矩阵
Incidence matrix (not commonly used)
一边是顶点,另一边是边。
<H4>列表表示</H4>
<H2>Lecture 22-23 GraphsTraversal: BFS and DFS</H2>
<H3>Subgraph</H3>
<H3>Traversing a Graph</H3>
<H4>图遍历基础</H4>
在图论中,遍历(traversal)或搜索(search)通常指的是访问图中的每个节点或边的过程。遍历算法经常用于解决各种问题,如路径查找、最短路径、图着色等。
<H4>递归遍历图</H4>
递归是一种强大的编程技术,通过函数自身调用来解决问题。在图遍历中,递归可以用来探索从一个给定节点出发的所有可能路径。
然而,使用递归遍历图时需要注意几个问题:
- 循环(Cycle):如果图中存在循环(即可以从一个节点出发经过一系列边后又回到该节点),那么递归调用可能会无限进行下去,导致栈溢出(stack overflow)。为了避免这种情况,需要跟踪已经访问过的节点,并在再次遇到这些节点时停止递归。
- 组合爆炸(Combinatorial Explosion):即使在无环图中,递归也可能导致组合爆炸。这是因为对于每个节点,我们都需要考虑从其出发的所有可能路径。对于具有许多分支的图,这可能导致大量的递归调用和巨大的计算量。
<H4>示例:二叉树与完全二叉树</H4>
给出的示例中,使用了一个类似于完全二叉树(complete binary tree)的图结构来说明组合爆炸问题。在这个例子中,每个节点都有两个子节点(除了最后一层的叶子节点),因此从根节点(标记为“0”)开始,递归遍历所有路径的数量是指数级的(O(2^N))。
这里就引出了两个搜索算法BFS&DFS
Two common graph traversal algorithms
Depth first search (DFS)
Breadth first search (BFS)
<H3>BFS</H3>
<H4>Applications</H4>
Find shortest path between nodes in unweighted graph
Cycle detection in undirected graph
GPS navigation for neighboring locations
Find person in social networks
Devices connected to a particular network
Crawlers in Search Engines
<H4>原理</H4>
广度优先搜索(BFS):从图的某个顶点开始,访问此顶点,然后访问其所有未被访问的邻接顶点,并标记为已访问。之后,从这些邻接顶点中选择一个,继续访问其未被访问的邻接顶点,如此类推。
<H4>两种BFS流程的表示法</H4>
BFS(G, s)
G表示Graph
s是初始节点
n = 顶点数
m = 边数
<H4>Pseudocode </H4>
1 Initialization: Enqueue the starting node into a queue and mark it as visited
1 将起始节点排队到队列中,并将其标记为已访问
这个步骤确保了BFS从指定的起始节点开始。
队列用于存储待访问的节点。
需要一个数据结构(如集合或哈希表)来跟踪已访问的节点,以避免重复访问。
2 Exploration: While the queue is not empty
Dequeue a node from the queue and visit it (e.g., print its value)
For each unvisited neighbor of the dequeued node:
Enqueue the neighbor into the queue
Mark the neighbor as visited
这是一个循环,确保所有在队列中的节点都被处理。
从队列中取出一个节点,并访问它。这通常是BFS的核心操作,因为它决定了搜索的顺序。
遍历当前节点的所有邻居。
需要检查邻居是否已被访问过,以避免重复访问。
将未访问的邻居入队,以便稍后进行访问。
更新已访问节点的记录,确保不会再次访问这个邻居。
3 Termination: Repeat Step 2 until the queue is empty
当队列为空时,说明没有更多的节点需要被访问,搜索结束。
<H4>Recap: Adjacency list vs Adjacency matrix</H4>
Adjacency list
More compact than adjacency matrix if graph has few edges
Requires a scan of adjacency list to check if an edge exists
Requires a scan to obtain all edges!
Adjacency matrix
Always require n^2 space
This can waste a lot of space if the number of edges are sparse
Find if an edge exist if O(1)
Obtain all edges in O(n^2)
1.初始化:
对于图中的每个顶点 u,将其标记为未访问(flag[u] = false)。
创建一个空的队列 Q 用于存储待访问的顶点。
将起始顶点 s 标记为已访问(flag[s] = true)并将其加入队列 Q。
2.广度优先遍历:
当队列 Q 不为空时,执行以下步骤:
从队列 Q 中取出一个顶点 v(DEQUEUE(Q, v))。
遍历与 v 相邻的所有顶点 w:
如果 w 尚未被访问(flag[w] = false),则将其标记为已访问(flag[w] = true)并将其加入队列 Q。
1.初始化:
与邻接列表相同,为图中的每个顶点u设置访问标志flag[u]为false。
创建一个空的队列Q。
将起始顶点s的访问标志设置为true并将其加入队列Q。
2.广度优先遍历:
当队列Q不为空时,执行以下步骤:
从队列Q中取出一个顶点v。
遍历邻接矩阵的第v行(或列,取决于邻接矩阵如何定义)来查找与v相邻的顶点。
对于每个相邻的顶点w,如果其访问标志为false,则将其访问标志设置为true并将其加入队列Q。
<H3>DFS</H3>
<H4>Applications</H4>
Traverse and return value (such as max, min, etc.)
Find a path from point A to B
Find connected components
Detect looping (cycles) and
Solve combinatorial problems, such as:
How may ways are there to arrange something
Find all possible combinations of . . .
Find all solutions to a puzzle。
<H4>原理</H4>
深度优先搜索(DFS):从图的某个顶点开始,访问此顶点,然后递归地访问其所有未被访问的邻接顶点。这个过程一直进行到已发现从源顶点到可达的所有顶点为止。如果还存在未被发现的顶点,则选择其中一个作为源顶点并重复以上过程
<H4>两种DFS流程的表示法</H4>
<H4>Pseudocode </H4>
1 Start by putting the source node on the top of a stack
1 首先将源节点放在堆栈的顶部
2 Take the top node of the stack and add it to the visited list
2 获取堆栈的顶部节点并将其添加到访问列表中
3 Create a list of that vertex’s adjacent nodes and add the ones which are not visited to the top of the stack
3 创建该顶点相邻节点的列表,并将未访问的节点添加到堆栈顶部
4 Keep repeating steps 2 and 3 until the stack is empty
4 继续重复步骤 2 和 3,直到堆栈为空
<H4>DFS(G)</H4>
1.初始化:
对于图中的每个顶点u
(应该使用v
或者其他变量来避免混淆,因为你后面用到了v
),将对应的flag[v]
设置为false
,表示该顶点尚未被访问。
2.调用递归DFS:
对于图中的某个起始顶点(不一定是每个顶点),调用递归的RDFS
函数。但根据描述,你似乎为每个顶点都调用了RDFS
,这在实际应用中是不必要的。
<H4>RDFS(v) </H4>
标记顶点为已访问:
将flag[v]
设置为true
,表示顶点v
已经被访问。
递归访问相邻顶点:
-
- 对于与
v
相邻的每个顶点w
,如果w
尚未被访问(即flag[w] = false
),则递归调用RDFS(w)
。
- 对于与
<H4>时间复杂度分析</H4>
DFS的运行时间与图中的边数和顶点数成正比,即O(n + m)或O(|V| + |E|),其中n是顶点数,m是边数。这意味着,无论图是稀疏的还是稠密的,DFS都会检查所有的边和顶点(至少一次)。
BFS | DFS | ||
Definition | 遍历从根节点开始,遍历同一级别上的所有节点,然后再移动到下一级别 | 遍历从根节点开始,并尽可能地穿过节点,直到到达没有未访问的附近节点的节点 | |
Conceptual Difference | Builds the tree 逐级(层) | Builds the tree 逐个子树 | |
Data structure | Queue(FIFO) | Stack(LIFO) | |
Suitable for | 搜索更靠近给定源的顶点 | 查找远离源的路径 | |
Applications | 寻找最短路径、二分图、GPS导航等。 | 循环或环路检测、寻找强连接组件(SCC)等。 | |
Path generation | 根据树级别进行遍历 | 根据树深度的遍历 | |
Backtracking | No | Yes | |
Memory | More | Less | |
Loops | 不能困在循环中 | 可能困在循环中 | |
Adjacency list | Time complexity | O(|V| + |E|) | O(|V| + |E|) |
Auxiliary space | O(|V| + |E|) | O(|V| + |E|) | |
Adjacency matrix | Time complexity | O(|V|^2) | O(|V|^2) |
Auxiliary space | O(|V|^2) | O(|V|^2) |
回溯Backtracking:当算法在尝试解决问题的过程中,发现当前的选择或路径无法继续或者不符合问题的要求时,会返回到上一个决策点,重新选择其他可能的路径进行尝试。
<H2>Tutorial 03 Hashtables & Graphs</H2>
<H3>Hashtables</H3>
<H4>Problem 1 单链哈希</H4>
Insert the keys E A S Y Q U T I O N in that order into an initially empty hashtable of M = 5 and hash function h(k) = (11k + 3)%5 using separate chaining. (Assume A = 1, B = 2, C = 3, . . . )
<H4>Problem 2 开放寻址哈希</H4>
Conisder inserting the keys 10, 22, 31, 4, 15, 28, 17, 88, 59 into a hashtable of length m = 11 using open addressing. Illustrate the results of these keys using
使用开放寻址将密钥 10、22、31、4、15、28、17、88、59 插入到长度为 m = 11 的哈希表中。说明这些键的结果
(i)linear probing with h(k, i) = (k + i) mod m, and (ii) double hashing with h1(k) = k and h2(k) = 1 + (k mod (m − 1))
mol的数就到下一位
额外加值进行哈希
选择典型的mol作为哈希值
<H4>Problem 3 开放寻址哈希</H4>
Which of the following hash functions will distribute keys most uniformly over 10 buckets number 0 to 9 for k ranging from 0 to 2020? (Hint: You may want to create a spreadsheet, calculate and see how the distribution of the computed hash values look like)
A. h(k) = (12 × k) mod 10
B. h(k) = (11 × k^2) mod 10
C. h(k) = k^3 mod 10
D. h(k) = k^2 mod 10
Briefly explain your answers.
<H3>Graphs</H3>
<H3>Problem 4 Graph</H3>
What is the maximum number of edges in a graph with V vertices? What is the minimum number of edges in a graph with V vertices, none of which are isolated?
若干点的最多的边和最少的边问题
(a) Maximum number of edges in a fully connected graph = (n − 1) + (n − 2) + · · · + 1 = (n)(n − 1) /2
(b) Minimum number of edges in a connected graph = n − 1 (same as a linked list)
<H3>Problem 5 Graph表示法</H3>
Draw the adjacency matrix and adjacency list of the graph below.
<H3>Problem 6 BFS</H3>
Suppose you have the following directed graph
Start from node S, write down the path generated if breadth first search (BFS) is used. Break all ties by picking the nodes in alphabetic order.
S, A, B, C, D, E, G2, G1
别忘了广度是一层一层的进行排序
<H3>Problem 7 Stack & Queue & BFS</H3>
Suppose you use a stack instead of a queue when running breadth first search (BFS). Does it still compute shortest paths?
Stack LIFO
Queue FIFO
If a stack is used when running a BFS, we will have DFS, which will pick one path, and go in it as deep as it can, then pick another path and so on so forth, which is not optimal for finding the shortest path. This is because a node A could either be reach in 5 steps if you go from one path, or in 10 steps if you go from another path. The DFS might pick the long path and then keep going into it until it find s the node and tells you that this is the path to the node, even though there is a much shorter path that the DFS did not explore yet. We have te continue the set (of results) until the shortest path has been found, but this is too much time wasted.
使用队列的目的是确保我们按照到达节点的顺序(即“层”的顺序)来访问它们。
如果我们使用栈,那么后到达的节点会先被处理。
为了更直观地理解这一点,可以想象一个迷宫,从一个入口开始,并希望找到到达某个出口的最短路径。使用BFS和队列就像首先探索所有直接相邻的房间,然后对每个已探索的房间,再探索它们各自的相邻房间(但之前未被探索过)。这种情况下第一次碰到出口的话,肯定就是入口到出口的最短路径。
但是,如果使用栈,那么可能会先深入某个分支,然后再回到其他分支,这样会使搜索的深度增加,而且这可能会使错过更短的路径。
It should be noted that we can simulate the function of a queue using two stacks (which has been discussed in the lecture). In this situation, the same result of BFS can be computed, but a significant amount of execution time will be used to manipulate the data between the queues.
详细方案:
· 初始化:准备两个栈,一个用于模拟队列的入队操作(我们称之为inputStack),另一个用于模拟队列的出队操作(我们称之为outputStack)。开始时,两个栈都为空。
· 入队操作:在BFS中,当我们要访问一个节点的所有未访问邻居时,我们实际上是在将这些邻居入队。使用栈模拟时,我们将这些邻居节点压入inputStack。
· 出队操作:在BFS中,我们从队列中取出一个节点并访问它。使用栈模拟时,如果outputStack为空,我们就将inputStack中的所有元素逐个弹出并压入outputStack(这个操作实现了队列的“先进先出”原则,因为最后压入inputStack的元素会最先被弹出并压入outputStack)。然后,我们从outputStack的顶部弹出一个节点并访问它。
· 迭代:重复上述的入队和出队操作,直到所有可达的节点都被访问过
<H2>Final Review</H2>
<H3>Three major components</H3>
Problem solving and problem analysis 问题解决和问题分析
Data structures 数据结构
Algorithms 算法
<H3>Problem solving and problem analysis </H3>
<H4>Problem analysis</H4>
Problem and data abstraction
Inputs and outputs specification
Constraints and assumptions
<H4>Program design</H4>
Program decomposition
Divide problem into smaller subproblem that can be solved separately at different time, by different person
Data modelling and design
Choice of algorithms
Complexity and the asymptotic notation (Big-Oh (O), Big-Omega (Ω), and Big-Theta (Θ))
Algorithm tracing and verification
<H4>Problem solving techniques </H4>
Data abstraction and modelling
Data-directed design i.e., objects and abstract data type
(ADT)
Information hiding i.e., abstraction for external use hiding
internal detail, levels of abstraction, etc.
Divide-and-conquer
Divide the problem into smaller, more manageable
subproblems that looks similar to the initial problem
Then solve these subproblems and put their solutions
together to solve the original problem
Recursion
Stopping case
Recursion step
Eventuality
<H4>Abstract data type (ADT) (Classes) </H4>
Specification
Data members and methods (functions)
public, private, protected
Object lifetime and scope
Object creation (constructor) and destruction (destructor)
The this pointer
<H4>Basics of analysis </H4>
What is Big-Oh (O), Big-Omega (Ω), and Big-Theta (Θ)?
How to determine the relative relationship of two functions?
Refer to increasing rate of some standard functions,
such as: n, n lg n, n 2 , 2n , etc.
Practice on some examples from lecture notes, assignment, and
problems from the optional textbook!
How to analyze basic code segment, e.g., loop?
How to analyze the complexity of recursive functions?
e.g., use of recurrence equations
<H3>Data structures</H3>
<H4>Data type </H4>
Simple data types
Arrays
Classes
<H4>Abstract data types (ADTs) (Classes) </H4>
Sets
Heaps
Hashtables
Lists (or Collections)
Linked lists (singly linked lists, doubly
linked lists, and circular linked lists)
Stacks
Queues
Trees, such as, but not limited to, binary
tree
Graphs
<H4>Arrays and linked lists</H4>
Two fundamental ones: Arrays and linked lists
Three basic operations: search, insertion, and deletion
Arrays
What are the steps to resize an array?
<H4>Trees </H4>
Basic definitions and concepts
Path, length of a path, height and depth of a node
Tree traversal
Preorder
Postorder
Inorder
Searching, insertion and deletion in binary search tree
(BST)
How do they work?
What is the time complexity?
<H4>Heaps </H4>
A complete binary tree implemented with an array
How to build a max- (or min-) heap
Properties and application (e.g., priority queues)
Searching, insertion and deletion in heap
How do they work?
What is the time complexity?
<H4>Hashtables </H4>
Hash codes
Hash functions
Goal: to generate hash codes that are evenly distributed
What approaches can we use for different data types?
Use of prime number in hash function
Collision handling
Separate chaining
Open addressing
Linear probing – problem: Primary clustering
Quadratic probing – problem: Secondary clustering
Double hashing
How to expand a hashtable?
How does it different from resizing an array?
<H3>Algorithms </H3>
Searching
Linear search
Binary search
Binary search tree (BST) (Binary tree search)
Sorting
Comparison-based approach
Insertion sort, selection sort, bubble sort
Merge sort, quick sort (“divide-and-conquer”-based)
Heap sort (“tree”-based)
Non-comparison-based approach
Counting sort
Radix sort
Graph traversal
<H4>Sorting algorithms </H4>
How does the algorithm work?
First, you should have general idea on how the algorithm works
Then, read the pseudocode, which possibly including some implementation details.
Try to practice the algorithm execution on some examples
What is the time complexity?
You may need to identify the worst-case and best-case sometime.
Also, you should be careful about the complexity of recursive functions (be familiar with the techniques for solving such problems)
<H4>Complexities of Different Sorting algorithms </H4>
<H4>Graph </H4>
General graph concepts
Edge, vertex, degree, path, cycle , subgraph, connected component
Graph representation:
Adjacency matrix
Adjacency list
Pay attention to their features!
Path searching
Breadth first search (BFS)
Depth first search (DFS)
How does the algorithm work?
What is the time complexity with different graph
representation?
BFS | DFS | ||
Definition | 遍历从根节点开始,遍历同一级别上的所有节点,然后再移动到下一级别 | 遍历从根节点开始,并尽可能地穿过节点,直到到达没有未访问的附近节点的节点 | |
Conceptual Difference | Builds the tree 逐级(层) | Builds the tree 逐个子树 | |
Data structure | Queue(FIFO) | Stack(LIFO) | |
Suitable for | 搜索更靠近给定源的顶点 | 查找远离源的路径 | |
Applications | 寻找最短路径、二分图、GPS导航等。 | 循环或环路检测、寻找强连接组件(SCC)等。 | |
Path generation | 根据树级别进行遍历 | 根据树深度的遍历 | |
Backtracking | No | Yes | |
Memory | More | Less | |
Loops | 不能困在循环中 | 可能困在循环中 | |
Adjacency list | Time complexity | O(|V| + |E|) | O(|V| + |E|) |
Auxiliary space | O(|V| + |E|) | O(|V| + |E|) | |
Adjacency matrix | Time complexity | O(|V|^2) | O(|V|^2) |
Auxiliary space | O(|V|^2) | O(|V|^2) |
<H3>Suggested review methods</H3>
Read through all lecture notes carefully. Try to fully
understand all concepts, definitions, algorithms, and
analysiss
Don’t blindly remember the details without understanding it!
Practice the algorithm execution and complexity analysis by
yourself once
Review homework and assignment
Work on the problems available at the optional textbook and
reference books!
Note: Please do not recite the lecture notes!
You should be able to answer the questions based on your understanding!
<H2>My Note Final Part I</H2>
L1 Problem Analysis and Procedural Abstraction
L2 Optional class
Movezerotoend & whetherleapyear
Lab0 optional lab
L3 Analysis of Algorithms
L4 Analysis of Algorithms: Asymptotic Notations
渐进原则 讲述了三种算法时间的计算方法
Lab1 Complexity analysis
L5 Data Structures and Abstract Data Type
L6-7 Data Structures and Abstract Data Type Data Abstraction
L8 Recursion
Lab2 Recursion
<H2>My Note Final Part II</H2>
L9-12 Comparison Based Sorting
T01 Recursive & Code Tracing
Assignment1
L13 Non-comparison Based Sorting
L14 Linked lists
Lab3
L15 Stack & queue
Stack的应用
L16 Tree
L17 Trees Insertion and Deletion
BST
Balance & Unbalance Tree
T02
L18-20 Hashtables
L21 Graphs
L22-23 GraphsTraversal: BFS and DFS
例子:二叉树和完美二叉树
T03 Hashtables & Graphs