1.概括
本周主要学习了5个方面的知识,包括:
- Java基本程序设计结构:
- Java的程序要素与注释
- 数据类型
- 变量与常量
- 运算符
- 字符串
- 输出与输入
- 控制流程
- 大数
- 数组
- 数据结构中的线性表:
- 线性表的定义
- 线性表的抽象数据类型
- 线性表的顺序存储结构实现
- 线性表的链式存储结构实现(单链表)
- sql语法:
- sql语法基本介绍
- SELECT语句基本格式
- 检索结果的基本处理
- 计算机网络:
- IP、TCP、DNS协议简介
- HTTP报文的基本格式
- HTTP请求的方法
- 无状态协议
- Cooike
- HTTP报文的编码
- HTTP的多对象集合
- 正则表达式语法基本了解
1.Java的基本程序设计结构
3.1 Java程序要素
以下是一个简单的JavaSE程序,但是里面包含的内容会在Java所有程序中出现。
public class FirstProgram{
public static void main(Strng[] args){
System.out.println("goodbye,world!");
}
}
- Java区分大小写
- 开头的关键字public称为访问修饰符,用于控制程序的其他部分对这段代码的访问级别
- 关键字class表示这是一个类,Java是一种偏向工程的语言,一切代码都包含在类中。这里可以把类看作程序逻辑的一种容器
- class后面的紧跟着类名,Java的类名必须以字母开头,后面可以跟字母或数字,没有长度限制,但是不能使用Java保留字
- 但是有标准命名约定:类名采用大写字母开头的名词,之后的每个单词首字母也大写,称为驼峰命名法
- 源代码文件的命名必须与公共类名相同,用.java作为扩展(使用javac命令需要添加后缀)
- 当编译器完成编译后,得到的字节码文件后缀为.class,Java编译器会将.class文件放到与.java文件同一个目录结构中(使用java命令不用后缀)
- 当运行已经编译好的程序时,JVM总是从main方法开始执行,Java语言规范约定main方法必须声明为public
- Java中函数也被称为方法
- 方法的程序都属于一对{},Java中方法的所有代码都必须用{}包裹
- 大括号的风格怎样都行, Java编译器会忽略空白行
- static表明是静态的,main 方法总是静态的
- void表明main方法没有返回值,不需要return 0
- System.out是一个对象,调用了他的println()方法,Java中的调用语法是 : 对象.方法名();
- Java语句必须以分号结束
- println()方法接收一个字符串参数,并将其输出到控制台,之后自动换行
- 除了println(),还有一个print()方法,这个方法不会自动换行,此外,Java还有与c语言中几乎一样的printf()方法用来实现格式控制
- Java的方法可以没有参数,也可以用一个或多个参数
3.2 注释
Java有三种注释:
//这是一种注释,只能注释这一行后面的内容
/*这也是一种注释,可以包括较长的字符,
也可以跨行*/
/**
*这种注释用来自动生成文档
*/
注意:/**/不能嵌套使用,也就是在/**/不能再加入/*或*/。
3.3 数据类型
Java是一种强类型的语言,意味着每一个变量声明时都必须给出它的数据类型
Java有8种基本数据类型:4种整型、2种浮点型、1种布尔型和1种字符类型char
注:
- String字符串不是不是基本类型,而是String类的实例
- Java种有一种类 big number,它的对象可以表示任意精度,但是不是基本类型
3.3.1 整型
| 类型 | 存储需求 | 大致范围 |
|---|---|---|
| byte | 1字节 | -128~127 |
| short | 2字节 | -32768~32767 |
| int | 4字节 | 大约超过一点-20亿~+20亿 |
| long | 8字节 | 在1018数量级 |
Java中,数据类型的取值范围与平台无关,只要安装了JVM的机器,在执行Java字节码时,各种类型范围固定,得到的结果也是相同的
能更好的实现跨越平台
长整型数值后有后缀L或l,如4000000L。
十六进制数有前缀0X或0x。如0xFFFF
八进制数前缀是0,如010,这是八进制的8.这很容易混淆,一般不用八进制。
二进制有前缀0B或0b,如0b1001=9。
Java中没有无符号形式的int、short、long、byte等类型
但是,如果非常希望处理无符号数,比如想计算0~225范围无符号数,可以使用Byte.toUnsignedInt(b)方法,这个方法会得到0~
225的int值,然后把它转会byte型。
Inter类,Long类都提供了类似的方法来处理无符号数
3.3.2 浮点类型
| 类型 | 存储需求 | 大致范围 |
|---|---|---|
| float | 4字节 | 大约7位有效数字 |
| double | 8字节 | 大约15位有效数字 |
有效数字:从第一个非零数字开始,到最后一个具有实际意义的数(包括0,但不包括前导0或尾随的无意义0)
定点数中:
-
0.000456 三位有效数字 456
-
123.450 6位有效数字 123450
-
1.23000 5位有效数字12300
浮点数中:
例如,132456789.0
-
单精度可能会存储为1.234568*108实际有效数字只有7个
-
双精度可能存储为1.234567890123456*1015保持16位有效数字
由于float的有效数字太少,一般需要用浮点数时我们都使用double型
float型有一个后缀f或F,如果没有后缀的小数,一般都默认为double型
有三个比较特别的浮点数值表示溢出出错的情况:
Double.POSITITIVE_INFINITY:正无穷大
Dobule.NEGTIVE_INFITY:负无穷大
Dobule.NaN:不是数
一个整数除以0会得到正无穷大
计算0/0或负数的平方根会得到Double.NAN
注:不能用if(x == Double.NaN)检测一个结果是否为NaN。
因为每个NaN都被设计成不相同的
如果是金融计算等不能接收误差的业务,就不能使用浮点型,而应该使用BigDecimal类
3.3.3 char类型
char类型:存储空间是2字节
最初设计时,希望一个char类型能够表示1个Unicode编码的字符,但随着Unicode发展,2字节16位的空间可能无法表示一个Unicode字符了。
码点与代码单元
码点:
-
字符在Unicode中的唯一编号,表示为U+xxxx形式,如:
-
A 的码点是U+0041
-
‘中’的码点是U+4E2D
-
😊(笑脸)的码点是
U+1F60A
-
代码单元:
- UTF-16使用16位,2个字节作为基本存储单位,称为代码单元。
- 基本多语言平面 BMP:
- 码点范围:U+0000~U+FFFF
- BMP的字符都可以用一个16位代码单元存储,也就是一个char变量
- 辅助平面:
- 码点范围:U+10000~U+10FFFF
- 这些字符必须使用一对代码单元(称为代理对)存储
- 如:
- 😊(U+1F60A)的 UTF-16 编码是 两个代码单元:
- 高代理项(High Surrogate):0xD83D
- 低代理项(Low Surrogate):0xDE0A
Java中char类型与码点、代码单元的关系
单个char存储BMP中的字符:
char ch1 = 'A';
char ch2 = '中';
对于辅助平面的字符,必须用一对char类型(代理对)存储:
char ch3 = '\uD83D';
char ch4 = '\uDE0A';
String emoji = "\uD83D\uDEOA"; //存储了😊字符
总结:
| 概念 | 说明 | 例子 |
|---|---|---|
| 码点 CodePoint | Unicode字符的唯一编号 | A:U+0041 |
| 代码单元 | UTF-16存储字符的基本单位 | A只要一个代码单元,😊需要两个代码单元 |
| char类型 | Java的16位字符单元,存储UTF-16的代码单元 | char ch = ‘A’;char[] emjoe = {{‘\uD83D’, ‘\uDE0A’} |
| 码点和char | 码点是Unicode编号,而char是UTF-16存储单元 | char不能直接表示U+1F60A,必须使用char代理对 |
可见,char类型只能表示一个UTF-16的代码单元,而String类字符串可以处理完整的字符。所以,最好处理字符时不适用char而使用字符串。或者使用int类型变量存储字符的码点。
(可以用codePointAt()获取Unicode码点)
3.3.4 boolean类型
boolean类型变量只有两种取值true和false。Java中的布尔类型不能转换成整型的1或0
3.4 变量与常量
与所有程序设计语言一样,Java使用变量存储值,而常量,就是值不变的变量。
3.4.1 变量声明
在Java中,每个变量都有一个类型,声明时,先指定类型,再指定变量名。
int a;
char ch;
boolean isStundent;
声明必须用;结束
变量名区分大小写
变量名可以是字母数字货币符号和标点连接符(主要是__)组成。
注:
- 这里的字母不止是拉丁文字母,也可以是希腊字母、德语字母等
- 货币符号也不只美元$,也包括¥、€等
- 但最好不要在自己代码中使用$作为变量名,因为java编译器或其他工具生成的名字会用这个符号
- 单独的_是Java保留字,也不可以做变量名
声明可以声明一个变量,也可以声明多个变量,但不推荐一次声明多个变量的风格。
3.4.2 变量初始化
变量声明后,必须要用赋值语句初始化才能使用。
int a;
a = 0;
赋值语句:
变量名 = 表达式或值;
可以把声明和初始化放在同一行:
int a = 2;
Java的变量声明可以放在代码中任何地方,但建议靠近第一次使用变量的位置。
Java不区分变量的声明和定义。
c和c++中:
int i = 10; //变量的定义
extern int i; //变量的声明
var
从Java 10开始,对于局部变量,如果能从变量的初始值推断出它的数据类型,可以使用var关键字而不用指定数据类型:
var age = 12; //整型
var name = "小明"; //字符串
3.4.3 常量
Java中使用关键字final指示常量
final double CM_PRE_INCH = 2.54;
final表示这个变量在初始化后不能再赋值了
习惯上常量名全部用大写
在Java中可以创建一个常量以便在一个类中的多个方法中使用,这种常量称为类常量。
public class Example{
final static double CM_PRE_INCH = 2.54;
public static void main(String[] args){
double a = CM_PRE_INCH *3;
}
}
类常量需要在方法外声明,这样类中其他方法也可以使用这个常量
如果一个常量被声明位public,那么其他类的方法也可以使用这个常量
注:const是Java保留字,但目前没有使用
3.4.4 枚举类型
枚举是一种特殊的数据类型,用以定义一组命名的常量。
枚举可以提高代码的可读性、安全性和可维护性。
Java中使用enum声明枚举类型
enum Day{MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY}
枚举的优势
1.提高代码可读性
枚举可以给常量赋予有意义的名称,使代码更好理解,避免了常量命名不清晰的问题
// 使用枚举
enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
Day today = Day.MONDAY;
// 使用整数常量
int today = 1; // 1 代表星期一
// 使用字符串常量
String today = "MONDAY";
2.增强类型安全
编译器会自动检查枚举值的有效性,避免无效赋值
Day today = Day.MONDAY; //正确
today = 1;//编译错误:类型不配
3.简化代码逻辑
枚举类型可以把相关的常量组织在一起,并提供方法或属性操作这些常量
enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
public boolean isWeekend() {
return this == SATURDAY || this == SUNDAY;
}
}
Day today = Day.SATURDAY;
System.out.println(today.isWeekend()); // 输出: true
4.支持高级特性
枚举可以定义方法、实现接口、添加字段等,使其功能更强大
5.避免重复定义
枚举将常量集中在一个地方,避免了重复定义和分散管理
6.支持遍历
枚举可以用foreach遍历
for(Day day :Day.values()){
System.out.println(day);
}
7.工具和框架支持
许多现代框架如Spring和工具如JUnit都支持枚举类型
8.避免硬编码
枚举可以减少代码中的硬编码,使代码更易于维护和扩展
enum Status {
PENDING, APPROVED, REJECTED
}
Status status = Status.PENDING;
当需要添加新状态时,只用修改枚举定义,并不会影响到使用枚举的其他代码
9.枚举线程安全
枚举是线程安全的,因为在JVM中是单例的。
10.枚举支持序列化
枚举内置了序列化机制,不需要额外实现Serializable接口,使得枚举在网络传输或持久化时更加方便
3.5 运算符
运算符用于连接值。
3.5.1算术运算符
算术运算符包括+ - *和\以及取模运算%
对于\:
只有当两个操作数都是整数时,表示整数除法,否则都会变成浮点数除法
3.5.1取模运算
作用:计算两个数相除后的余数
1.定义
a%b = r,其中:
- a:被除数
- b:除数
- r:余数,满足0<=r<b
2.取模运算的作用
利用取模运算的特性,可以运用于计算机程序设计的多种场合:
-
判断奇偶性
-
通过取模运算可以快速判断奇偶
-
int number = 7; if(number%2==0){ ....... }
-
-
遍历循环
-
只要除数不变,即使被除数变化,取模运算的结果也总是保持0到除数-1的循环,利用这个机制,可以实现循环索引
-
int[] array = {1, 2, 3, 4, 5}; for (int i = 0; i < 10; i++) { int index = i % array.length; // 循环索引 System.out.println(array[index]); }
-
-
限制范围
-
取模运算可以把一个数限制在某个范围
-
int value = 15; int range = 10; int result = value % range; // 将 15 限制在 0 到 9 之间 System.out.println(result); // 输出: 5
-
-
哈希函数
- 取模运算常用于哈希函数中,将大范围的数映射到固定范围
-
时间计算
-
取模运算可以用于时间单位的转换,例如将秒转换成分钟:秒格式
-
int totalSeconds = 125; int minutes = totalSeconds / 60; // 分钟 int seconds = totalSeconds % 60; // 秒 System.out.println(minutes + " 分钟 " + seconds + " 秒"); // 输出: 2 分钟 5 秒
-
-
周期性操作
- 取模运算可以运用于处理周期性操作,例如队列的实现
-
加密算法
- 取模运算在加密算法(如RSA)中也有很多应用
3.特性
取模的结果始终保持0~b-1(b是除数)
负数取模:当被除数为负数,取模的结果取决于编程语言的实现。Java中,结果的符号与被除数一致。
System.out.println(-10 % 3); // 输出: -1
System.out.println(10 % -3); // 输出: 1
System.out.println(-10 % -3); // 输出: -1
(注:从数学上来讲,取模运算的结果应该与除数一致,而取余运算才是符号与被除数一致。所以严格来说Java实现的其实是取余运算)
0%任意非零数 = 0
任意数% 0 是未定义的除零行为,会报错
3.5.2 数学函数与常量
在Java中,大部分可能会使用的数学函数都属于Math类。
想使用Math类中的方法,用两种格式:
double x =4;
double y = Math.sqrt(x);
或者直接在源文件中引入Math类:
import static java.lang.Math.*;
double x = 4;
double y = sqrt(x);
注:Math类中的方法大部分是静态方法,不处理任何对象
常见的Math方法:
- sqrt(x):计算x的平方根并返回
- pow(x,a):计算x的a次幂(xa),Java中没有幂运算运算符,必须用这个方法
- 常见的三角函数:
- Math.sin
- Math.cos
- Math.tan
- Math.atan
- Math.atan2
- 常见的对数和指数函数:
- Math.exp
- Math.log
- Math.log10
- 表示Π和e的常量:
- Math.PI
- Math.E
注:
- Math中涉及到除法的方法都使用的是浮点除法,如果希望实现完全准确的结果,应该使用StrictMath类,这个类能确保在所有平台得到完全相同的结果。
- 使用Math.multiplyExact()等方法也可以计算加减乘除等,同时能在发生溢出时抛出异常,而直接使用运算符则不会,例如若使用int型计算10亿*3,只会把溢出导致的错误结果存回变量中,这种错误编译器无法发现。
3.5.3 数值类型之间的转换

实线代表了可以实现没有精度损失的转换
而虚线代表了可能会造成精度损失
原则上来说,存储要求小的数据类型转到存储要求大的不会有损失,反之则可能造成损失,这很好理解,就像小孩子可以睡大人床,但成年人不能谁的下婴儿床。
但是要注意:虽然int和float存储要求都是4个字节,但是从int转向float会有精度损失。因为float只有7位有数字,但Int的范围要超过20亿也就是9位有效数字。long转换成double也是类似的道理。
但是虽然有可能造成精度损失,float本身采用科学计数法,能够表示的范围能到1038左右,范围远比int大,所以int到float仍然能自动类新版转换。
在进行二元运算时,首先会自动把两个操作数转换成相同数据类型,再进行计算,转换的原则是:
- 如果有一个操作数是double,都转换成double类型;
- 否则,如果有一个操作数是float类型,都转换成float类型;
- 否则,如果有一个操作数是long类型,都转换成long类型;
- 否则,都转换成int类型。
3.5.4 强制类型转换
如果确实需要将一种大范围的数据类型转换成小范围的话,就需要强制类型转换。
语法:
double x =5.99;
int nx = (int)x ; //nx = 5
当把浮点数转换成整型时,会舍弃所有小数部分
如果希望按照舍入的规则进行转换,可以使用Math.round方法
double x = 9.97;
int nx = (int)Math.round(x); //此时nx=10
要注意的时Math.round()方法返回的对象也是double型,需要再对结果进行一次强制类型转换
3.5.5 赋值
赋值语句本身就是一种表达式,因此赋值语句本身也会有一个值,这个值就等于=右边的值
int a,b;
a=(b=4); //这里"b=4"这个式子值为4,所以a的值也赋成了4
逻辑上与数学的连等差不多,但是赋值表达式和数学的等式是完全不同的,数学上不存在x=x+1这种表达,赋值表达式完全可以
在赋值语句中,可以使用二元运算,比如+= -= *= /= %= …
比如 :
x+=1; //等价于 x=x+1
3.5.6 自增自减运算
Java中也支持使用++或--表示自增自减,规则也是同样的:
前缀时表示先自增自减再参与其他运算;
后缀时表示先参与其他运算再自增自减
int m =7;
int n =2;
int result = m*++n; //result = 21
int result2 = m*n++; //result2 = 14
但这种写法降低代码可读性,容易引起他人误解,并不推荐
3.5.7 关系运算和boolean运算
| 符号 | 解释 |
|---|---|
| == | 两边的式子的值是否相等 |
| != | 两边的式子的值是否不相等 |
| && | 两边的式子都为真时表达式为真,否则都为假 |
| || | 两边的式子都为假时表达式为假,否则取值为真 |
&& 和 ||采用了短路策略:
- &&左边的式子如果为假就不再运算右边的式子直接取值false
- ||左边的式子如果为真就不再运算右边的式子直接取值为true
- 可以利用这个策略避免计算/0等,比如: if(y!=0 &&x/y>3)….这样只要y=0就不会再去计算右边x/y
3.5.8 条件运算符
java提供了 条件? : 运算符,也成为三目运算符。
格式:
条件?表达式1 : 表达式2
当条件为真时,整个运算取值为表达式1的值,否则取值为表达式2的值
3.5.9 switch 表达式
switch表达式是java14引进的,当我们需要在多个值做出选择时,可以用这种表达式。
要注意的是switch表达式功能和结构与switch语句类似,但是switch表达式的本质是一种表达式,是有值的,而switch语句是一种控制语句,只能执行代码。
语法:
int seasonCode = 1;
String seasonName = switch(season){
case 0 -> "Spring";
case 1 -> "Summer";
case 2 -> "Fall";
case 3 -> "Winter";
default -> "???";
}
case标签除了整型外还可以是字符串字面量或者枚举常量
case标签后可以提供多个值,用,分隔
int numbers = switch(condition){
case 1,2,3 -> 6;
case 4 -> 100;
default -> 999;
}
如果case标签后是枚举常量,不需要设计default,同时也不需要给出枚举名,但如果是整型或字符串,必须设计default,因为switch表达式必须有一个值
enum Size {"SMALL","MINDIM","LARGE","EXARE_LARGE"};
Size item = ...;
String label = switch(item){
case SMALL -> "s"; //这里的枚举常量不需要写成Size.SMALL形式
case MINDIM -> "m";
case LARGE -> "L";
case EXARE_LARGE -> "XL";
//不需要default
}
3.5.10 位运算符
Java中实现的位运算:
| 运算符 | 名称 | 功能描述 |
|---|---|---|
| & | 按位与 AND | 两个操作数对应位都是1时,结果位1,否则为0 |
| | | 按位或 OR | 两个操作数对应位有一个是1时,结果为1,否则为0 |
| ^ | 按位异或XOR | 两个操作数对应位不同时结果为1,否则为0 |
| ~ | 按位取反NOT | 将操作数每一位取反 |
| << | 左移 | 将左操作数的二进制位向左移动,移动右操作数指定的位数,低位补0 |
| >> | 带符号右移 | 将左操作数的二进制位向右移动,移动右操作数指定的位数,高位补符号位(正数补1负数补0) |
| >>> | 无符号右移 | 将左操作数二进制位向右移动,高位补0 |
位运算的典型应用
-
权限控制
-
用二进制位表示权限,如读、写、执行,通过位运算高效组合和校验权限
-
// 定义权限标志 int READ = 1 << 0; // 0001 int WRITE = 1 << 1; // 0010 int EXECUTE = 1 << 2;// 0100 // 组合权限 int userPermissions = READ | WRITE; // 0011(可读可写) // 检查权限 boolean canRead = (userPermissions & READ) != 0; // true boolean canExecute = (userPermissions & EXECUTE) != 0; // false
-
-
数据压缩与优化
-
用位运算代替乘除,提升性能,比如x<<3 = x*8
-
想象十进制中,一个数1234,把小数点向有移动两位
-
变成了123400,相当于1234*100
-
相对于小数点来说,数字向左移动了,所以叫做左移
-
int x = 5; int result1 = x << 3; // 相当于 5 * 8 = 40 int result2 = x >> 1; // 相当于 5 / 2 = 2(向下取整)
-
-
网络协议解析
-
解析二进制协议时,可以用位运算提取特定字段,如IP地址、报文头等
-
int packetHeader = 0xABCD1234; // 提取高 16 位 int version = (packetHeader >> 16) & 0xFFFF; // 0xABCD // 提取低 16 位 int data = packetHeader & 0xFFFF; // 0x1234
-
-
高效存储状态
-
用整数的二进制位表示多个布尔状态,可以节省内存
-
// 用 4 位表示 4 个开关状态 int switches = 0; // 打开第 2 个开关(从右往左数,索引从 0 开始) switches |= 1 << 1; // 0010 // 关闭第 3 个开关 switches &= ~(1 << 2); // 1111 -> 1011
-
-
哈希算法与加密
- 位异或常用于哈希计算或简单加密
注意:
- 位运算只能用于整型
- 移位操作受到数据类型的位数限制,比如int,右操作数的范围是0~31(4个字节只有32位,1位用来表示符号)
- 当超过这个范围时,就要对32做取模运算
- 如 1<<35 就等同于 1<<3
- 右移>>保留符号位,而>>>始终在高位补0
- 位运算会降低代码可读性,需要添加注释说明逻辑
位运算直接操作二进制位,比常规运算更快,适用于:
- 高频计算,如实时数据处理
- 内存敏感场景,如嵌入式系统或大规模数据处理
3.5.11 括号与运算符优先级
| 优先级 | 运算符 | 结合性 |
|---|---|---|
| 1 | [] . () | 从左向右 |
| 2 | ! ~ ++ -- +(表示正) -(表示负)(强制类型转换) new | 从右向左 |
| 3 | * / % | 从左向右 |
| 4 | + - | 从左向右 |
| 5 | << >> >>> | 从左向右 |
| 6 | == != | 从左向右 |
| 7 | & | 从左向右 |
| 8 | ^ | 从左向右 |
| 9 | | | 从左向右 |
| 10 | && | 从左向右 |
| 11 | || | 从左向右 |
| 12 | ?: | 从右向左 |
| 13 | = += -= *= /= %= &= |= ^= <<= >>= >>>= | 从右向左 |
总结:
优先级最高的是方法调用、括号
之后是一元运算符(包括表示正负的±)
之后是算术运算 乘除>±
之后是移位运算
之后是判断相等
之后是位运算 与>异或>或
之后是逻辑运算 与>或
之后是三目运算
最后是赋值运算
3.6 字符串
从概念上讲,字符串就是Unicode字符的集合
从本质上讲,字符串是Java预定义类String的实例对象,String类属于Java标准库,不需要在源文件中额外引入
每个双引号括起来的内容都是String的实例
不能讲String理解成char的数组,如果从c语言角度思考,Java的字符串更像是:
char * str = "字符串";
3.6.1 子串
String类的subString()可以从一个大的字符串中提取子串:
String greeting = "Hello";
String s = greeting.subString(0,3); //s = "Hel"
subString方法:
- String subString(int beginIndex)
- String subSrting(int beginIndex,int endIndex)
- 返回一个新的字符串,这个字符串包含从beginIndex到字符串末尾,或者endIndex-1的所有代码
- 这个方法很容易得到字符串长度,length = endIndex-beginIndex
3.6.2 拼接
Java语言使用+来来连接两个字符串,用于组成一个新的字符串
新的字符串完全按照拼接时的顺序:
String str = "Woshi";
String name = "Xiaoming";
String introduce = str+name; //introduce = "WoshiXiaoMing"
可以讲任意一个非字符串的值与字符串使用+拼接,会自动转换成字符串
实际上,任何一个Java对象都可以转换成字符串
如果希望连接多个字符,并增加一个分割符,可以使用静态的String.join()方法
String all = String.join("/","S","M","L","XL");//all = "S/M/L/XL"
在 Java 11中,有一个动态repeat方法用来重复字符串
String repeated = "Java".repeat(3); //repeted = "JavaJavaJava"
3.6.3 字符串不可变
String类没有提供任何方法来对字符串进行修改,但可以修改字符串变量的值
例:
String str = “Hello” ;//字符串“Hello”就一定没有办法修改
str = str.subString(0,3)+"p!"; //str = "Help!"
点类似于c语言中的指针与结构体的关系,可以让指针指向不同的结构体,但作为字符串的那一块结构体java不允许修改
//java 中修改字符串变量类似于以下操作:
char* greeting = "Hello";
char* temp = (char*)malloc(sizeof(char)*6);
strncpy(temp,greeting,3);
strncpy(temp+3,"p!",3);
greeting = temp;
3.6.4 检测字符串是否相等
可以使用equals方法检测两个字符串是否相等
s.equals(t);
相等返回true,否则返回false
这里的s和t可以是字符串变量,也可以是字符串字面量,如“hello”.equals(str)
如果希望检查两个字符串是否相等并且忽略大小写,可以使用equalsIgnoreCase方法,如”Hello“.equalsIgnoreCase(“hello”);
不能使用 ==运算符检测两个字符串是否相等
因为 == 不会比较字符串是否存储在同一个位置,而不是去比较字符串的内容
如果字符串真的在同一位置上,那么理论上它们确实相等
但是JVM中,只会共享字符串字面量,不会共享+或subString()等生成的新字符串
也就是说:
String greeting = "Hello";
if(greeting == "Hello")... //这个有可能是真的
if(greeting.subString(0,3) == "Hel") //这个判定很可能并不能通过
这样的Bug是编译器无法检查的
因此,Java中判断字符串相等应该始终调用equals方法
(还有一个compareTo方法,类似于c语言中的strcmp方法)
if(greeting.compaerTo("Hello") == 0)
//按照字典顺序,如果greeting位于"Hello"之前,返回一个负数,如果在"Hello之后返回一个正数,如果相等返回0"
虽然这个方法也能判断字符串相等,但看起啦确实不如equals清晰
3.6.5 空串与null
空串也是一种字符串,有它的长度0,和内容(空)
但是,String类变量可以存储一个特殊值null,表示这个变量目前不与任何对象关联,不能在null值上调用方法
检测一个字符串是否是null,可以使用 ==
if(str == null)
但是,如果希望检测一个字符串既不是null也不是空串,就要配合equals方法:
if(str != null && !str.equals(“”))
3.6.6 字符串长度与代码单元与码点
Java字符串是char类型的值的序列
char类型变量存储的是Unicode的代码单元,BMP字符只需要一个代码单元,而辅助平面的字符需要两个char类型,也就是两个代码单元
String的length方法返回的也是是字符串代码单元个数
想要得到严格意义上的字符个数,也就是码点的个数,需要调用codePointCount方法
int cpCount = greeting.codePointCount(0,greeting.length());//返回0到greeting.length()-1之间的码点个数
想得到第i个码点,可以使用以下语句:
int index = greeting.offsetByCodePoints(0,i);//返回从0开始,i个码点后的码点索引
int cp = greeting.codePointAt(index);//返回给定位置的码点
如果希望得到一整个字符串的码点,可以使用codePoints方法,它会生成一个int值的流,每一个int值对应一个码点,然后使用toArray()转换成数组
int[] codePoints = str.codePoints().toArray();
//反过来也可以把一个码点数组转换成字符串
String str2 = new String(codePoints,0,codePoints.length);
总之,要始终牢记一个原则,对于字符类型的数据,都建议用String类型处理而不是char类型。
3.6.7 构建字符串
有时,如果需要多次的用短的字符串构建新的字符串,例如,按键或者从文件中读取单词
使用拼接的方法效率比较低,每次拼接都会创造一个新的字符串对象
可以使用StringBuilder (称为字符串构建器)类来处理需要频繁修改的情况:
StringBuilder builder = new StringBuilder(); //先构建一个新的字符串构建器
//当每次需要添加时,调用append方法
builder.append(ch); //可以添加单个字符
builder.append(str); //可以添加字符串
//当构建完成时,需要调用toString()方法将StringBuilder类对象转换成String对象
String result = builder.toSting();
注:Sting类和StringBulider类都有很多方法,有些比较常用,有些可能不经常用但关键的时候可能非常需要这些方法。
死记硬背没有意义,要多查阅Java的API解释文档
3.6.8 文本块
Java 15新增加了文本块机制
主要用来实现跨越多行的字符串
语法:
String text = """
Hello
world
""";
使用一对三个双引号标记。
文本块开头三个双引号后需要换行,这个换行符不计入字符串的内容,之后的换行符都会被视作字符串的真实内容
最终,text的值具体为:“Hello\nworld\n”
文本块比较适合编写其他语言的代码如SQL或者HTML
常规字符串中的转义序列在文本块中也能使用
文本块有一个单独的转义序列\,表示忽略掉这个换行符
比如:
"""
Hello \
goodebye.
""";
等同于“Hellogoodebye”
文本块会标准化结束符,删除末尾的空白符,并把windows系统的\r\n换行符简化为\n
对于缩进,也就是放在文字前面的空白符,文本块会删除公共空白符(就是作为一个语句本身的缩进)然后保留内部的
3.7 输入与输出
3.7.1读取输入
在Java中,将输出打印到标准输出流(控制台窗口)非常简单,只需要调用System.out.println
但读取标准输入流System.in稍微要复杂一些
//首先,要创建一个与标准输入流关联的Scanner对象
Scanner in = new Scanner(System.in);
//然后,使用Scanner的各种方法读取输入
String name = in.nextLine(); //读取一行输入
String firstName = in.next(); //读取一个单词,以空格作为分隔
int age = in.nextInt(); //读取一个整数
double monery = in.nextDouble();//读取一个浮点数
注:Scanner类属于java.util包,并不在Java标准库,使用Scanner类需要先用import引入
Scanner类的输入是可见的,所以不适合从控制台读取密码
想要读取密码,可以使用Console类
Console cons = System.console();
String username = cons.readLine("User name:");
char[] passwd = cons.readPassword("Password:");
为了安全起见,将密码存储在char数组中而不是字符串中,完成对密码处理后,应该马上用一个填充数组覆盖数组元素
使用Console对象处理输入不如Scanner类对象方便,Console对象必须一次读取一行输入,而且没有办法读取单个单词或者整数等
3.7.2 格式化输出
为了控制字符的输出格式,Java中也实现了printf方法,用法与c语言中基本一致,都是用格式说明符占位,然后跟参数的形式
System.out.printf("Hello,%s",name);
格式说明符格式:
%+参数索引(可选)+$(可选)+标志+宽度(可选)+.+精度+转换字符
参数索引:
在printf方法中,可以使用$指定参数的位置,即参数索引,这样即使参数顺序不同,也可以按照指定的顺序进行格式化
public class PrintfExample {
public static void main(String[] args) {
System.out.printf("%2$d, %1$d, %3$d%\n", 10, 20, 30);
}
}
//输出: 20,10,30
%2$d→ 输出第二个参数(20)
%1$d → 输出第一个参数(10)
%3$d → 输出第三个参数(30)
如果不使用$,参数会按照默认顺序依次替换格式说明符
标志:
| 标志 | 作用 | 示例 |
|---|---|---|
| + | 打印正负号 | +333.3 |
| 空格 | 在正数前面加一个空格 | | 333.3| |
| 0 | 增加前导0 | 00333.3 |
| - | 字段左对齐 | |333.3 | |
| ( | 将负数包在括号里 | (333.3) |
| , | 增加分组分隔符 | 3,333.33 |
| #(对于定点浮点数格式) | 总是包含一个小数点 | 3333. |
| #(对于十六进制或八进制) | 总是包含前缀0X或0 | 0XCAF1 |
| $ | 指定参数索引 | |
| < | 重复使用上一个参数 |
<标志:重复使用上一个参数
例如:
public class PrintfExample {
public static void main(String[] args) {
System.out.printf("%d, %<o, %<x", 16);
}
}
//输出: 16,20,10
说明:
%d =>以十进制格式输出16
%<0 => 以八进制格式重复上一个参数 16 结果是 20
%<x => 以十六进制格式重复上一个参数 16 结果是 10
转换字符:
| 转换字符 | 类型 | 示例 |
|---|---|---|
| d | 十进制整数 | 159 |
| x或X | 十六进制整数 | 9f |
| 0 | 八进制整数 | 237 |
| f或F | 定点浮点数 | 25.6 |
| e或E | 指数浮点数 | 1.59e+01 |
| g或G | 通用浮点数 (输出e和f中较短的情况) | |
| a或A | 十六进制浮点数 | 0x1.fccdp3 |
| s或S | 字符串 | hello |
| c或C | 字符 | H |
| b或B | 布尔 | true |
| h或H | 散列码 | 42628b2 |
| % | 百分号 |
可以多个标志组合控制,例如
System.out.printf("%,.2f",1000.0/3.0)
//输出 3,333.33
3.7.3 文件输入与输出
可以使用Scanner对象读取一个文件
Scanner in = new Scanner(Path.of("mytext.txt"),StandardCharsets.UTF_8);
这里第一个参数指定文件的路径,必须要用Path.of来包含路径,否则Scanner会把输入的路径理解成一个字符串而不是你想读取的文件
第二个参数指定读取文件的编码标准
之后就可以用Scanner类的方法读取文件了
想要写入文件,需要构造一个PrintWriter对象。
PrintWriter out = new PrintWriter("myfile.txt",StandardCharsets.UTF_8);
构造时需要提供文件名和字符编码
如果文件不存在,会创建这个文件。
PrintWriter同样有print,println,printf方法
3.8 控制流程
Java可以使用条件语句和循环结构来确定控制流程
3.8.1 块作用域
块 block :由若干条Java语句组成,并用一对大括号括起来,块的主要作用是确定变量的作用域
一个块中可以嵌套另一个块
public static void main(String[] args){
int n; //作用域是整个mian方法
{
int k; //k的作用域只在这个大括号中
}
}
注意:Java中不允许重定义变量,也就是说,内层块里面不不能声明和外层块中同名的变量。(c++中是可以的,内层定义会覆盖外层定义)
可以在Java程序结构中原本只能放一条语句的地方放置块语句
3.8.2 条件语句
Java中,条件语句的格式为:
if(条件)语句
条件必须用小括号括起来,当条件的值为true时,执行这个语句,这个语句也可以是块语句
if常常搭配else
if(条件){
….
}else{
……
}
else表示if中条件不成立时才能执行的语句或块
即使有多个if,else也只与最近的if组成一组
if(x<=0)if(x==0) sign = 0; else sign =-1;
else与第二个if配对,只有x<0时sign才等于-1
最好使用大括号让代码更加清晰
也可以反复使用多个 if…else if …
if(age >= 5){
...
}
else if(age <5 && age >1){
...
}
else if (age =<1){
.....
}
注意,多个else if之间的条件应该是互斥的
3.8.3 循环
while(条件) 语句
while会在条件为真时执行语句,这里的语句可以是代码块
每一次while循环都是先判断条件再执行,当条件变为false就会退出;如果一开始条件就是false,那么while一次也不会执行
如果希望至少执行一次,可以使用do 语句 while(条件)
do while 会先执行代码再进行循环
3.8.4 确定性循环
for循环是支持迭代的一种通用结构循环
Java中for循环语法差异不大
格式:
for(计数器初始化;循环条件;如何更新计数器){
循环体
}
注意:只有当循环体执行完后才更新计数器
Java允许在for循环的各个部分放置任意表达式
但是一般规定3个部分应该保证对同一个计数器进行初始化、判断和更新
注:
- 判断循环条件时应该格外注意判断浮点数相等的情况
for(double x = 0;x!=10;x+=0.1)......
由于舍入误差和浮点数特性这很有可能变成一个死循环
因为二进制无法准确的表达0.1
所以x可能从9.999998突然变成10.000009
- 在循环体中声明的变量作用域只在循环体内部,因此如果希望在循环体之外使用循环计数器最终值等,就应该确保在循环体之外进行声明
事实上,for循环只是while循环的简化形式,理论上,任何一个for循环都可以转换成while循环
for(int i =0;i<10;i++){
System.out.println("hello");
}
//完全等价于
int i = 0;
while(i<10){
System.out.println("hello");
i++;
}
例:利用for循环计算组合数:
(组合数C(m,n)= m!/n!*(m-n)!;排列P(m,n)=m!/(m-n)!)
从1~50,50个数字中抽6个数抽奖:
(50*49*48*47*46*45)/(1*2*3*4*5*6)
步骤:
-
分析数学表达式
-
抽象出迭代原理 :n个数抽K: n * (n-1)*……*(n-k+1)/1*2*.…*k
-
利用for循环计数器进行替代
int result = 1; for(int i=1;i<=k;i++){ result = result*(n-i+1)/i; }
3.8.5 多重选择:switch语句
在处理一个表达式可能产生多个值时,使用ifelse控制有些笨拙,可以考虑switch语句,以及Java 14 引进的swtich表达式
经典switch语句:
int choice = ...;
switch(choice){
case 1 :
...;
break;
case 2:
...;
break;
default:
....;
break;
}
switch语句从与选项值匹配的case执行,知道遇到一个break,或者执行到语句结束;
如果没有匹配的case,则执行default标签
如果我们担心自己遗漏了break导致多个case被执行,可以在编译代码时添加-Xlint:fallthing选项:
javac -Xlint:fallthing wenjian.java
这样,如果某个分支缺少break语句,编译器就会给出警告
这种不加break标签让多个case标签执行的行为,我们也称为直通行为
如果在编译器添加了上述选项,又确实希望某些switch语句执行这种直通行为,可以给外围方法添加一个@SuppressWarnings(“fallthing”)注解
注解:为编译器或者处理Java源文件或类文件的工具提供信息的一种机制。
在Java14中,引入了新的switch形式–switch表达式
语法:
int result = switch(xuanze){
case "Spring" -> 1;
case "Summer" ->2;
default -> 0;
}
switch表达式不需要使用break就能避免直通行为发生
不能在一个switch表达式中混用->和:
switch表达式每一个分支都必须要产生一个值,如果希望在标签后不直接赋值而执行多条语句,就要使用{}和yield标签
// switch 表达式中的代码块
int num = 2;
String result = switch (num) {
case 1 -> "One";
case 2 -> {
System.out.println("Processing...");
yield "Two"; // 需要用 yield 返回值
}
default -> "Unknown";
};
System.out.println(result);
这里,也并不是说必须显式的写出yield,只要确保{}里的语句能够产生一个具体的值,比如赋值语句
例:
switch(xuanze){
case "tiaojian1" ->{
System.out.println("tiaojian1对应的输出内容");
number = 1;
}
case "tiaojian2","tiaojian3"->
number=2;
default ->
number = -1;
}
虽然上述的写法理论上没有错误,但是如果是每一个分支都是为一个变量赋值或方法调用然后计算值之类的,我们就应该把它优化成使用switch表达式对这个变量赋值
int number = switch(xuanze){
case "tiaojian1" ->1;
case "tiaojian2","tiaojian3" ->2;
default -> -1;
}
这样的写法代码更清晰,易读性更强
同时,可以在switch表达式中抛出一个异常:
default -> throw new IllegalArgumentException("异常的原因");
一般如果能实现switch表达式的环境,我们都应该使用switch表达式,switch表达式语法更清晰能避免遗漏break导致的直通行为。
3.8.6 Java中断控制流程语句
Java中goto是保留字,但是Java不支持goto,不过Java支持一种带标签的break语句用来跳出嵌套循环。
break
break本身的作用是用于退出循环或者case语句
但是不带标签的break只能跳出离break最近的一层循环
public class TestBreakDemo {
public static void main(String[] args) {
int num = 5;
int result = 0;
int count = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
count++;
System.out.println("count=" + count);
result++;
if (result > num) {
System.out.printf("循环结束,%d>%d\n", result, num);
break;
}
}
}
}
分析:
- 如果break能退出最外层循环,那么最终输出的结果应该是“循环结束,6>5”;
- 如果break只退出最能层循环,那么当输出一次“循环结束,6>5”之后,外层循环还没有结束,i=1,会再次进入,count=7后,再次退出,i=2,最后退出循环

由此证明了break只能跳出离自己最近的一层循环。
但是Java中支持给break添加标签的用法,可以跳出嵌套循环
标签必须紧跟在想跳出的最外层循环之前,并且紧跟一个冒号:
public class TestBreakDemo {
public static void main(String[] args) {
int num = 5;
int result = 0;
int count = 0;
good_bye:
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
count++;
System.out.println("count=" + count);
result++;
if (result > num) {
System.out.printf("循环结束,%d>%d\n", result, num);
break good_bye;
}
}
}
}

continue
与break类似,continue也会中断循环,但continue不会退出循环,而是回到最近的循环开始的位置
3.9 大数
如果基本的整数和浮点精度不满足需求,可以使用java.math包中的两个类BigInteger和BigDecimal
这两个类可以处理包含任意长度数字 序列的数值
BigInteger实现任意精度的整数运算
BigDecimal实现任意精度的浮点数运算
转换:
使用静态的valueOf方法可以将一个普通的数转换为大数
BigInteger a = BigInteger.valueOf(100);
对于更长的数,可以使用带字符串参数的字符串
BigInteger reallyBig = new BigInteger("1111421232412353124342");
注:BigDecimal类应该始终使用带字符串的构造器,但是BigDecimal类的构造器会发生舍入误差,
BigDecimal a = new BigDecimal(0.1);
//可能得到一个值0.1000000000000321124678
不能使用常规的运算符操作大数对象
需要内置的方法
加法: add
乘法:multiply
BigDecimal c = a.add(b); //c = a+b
BigInteger d = c.multiply(b.add(BigInteger.valueOf(2))); // d = c*(b+2)
3.10 数组
数组用来存储相同类型值的序列。
3.10.1 数组的声明
数组是一种数据结构,存储同一类型的值,通过一个整型索引可以任意访问数组中的值
声明时,需要指出数组类型,即元素类型+[],然后再加上数组名
int[] a;
上述的语句只是声明了一个int[]类型的变量a
想要真正的得到一个数组,应该使用new操作符初始化一个数组
int[] a = new int [100];
这里的长度并不要求是常量,可以是变量
数组的长度是不可变的,如果要经常改变数组的大小,就应该使用另一种数据结构——数组列表 array list。
注:
声明时可以写int[] a; 也可以写 int a[] ; 推荐第一种写法,这样可以将数据类型和变量名较为清晰的分开
Java中,有创建并初始化数组的简写形式:
int[] a = {1,2,3,4,5,6};
这种语法不需要使用new,也不需要指出数组的长度,同时最后一个元素也可以再加一个逗号,方便以后再添加新元素
还可以声明匿名数组:
new int[] {2,4,6,8,10};
这种语法主要用于重新初始化一个数组或者传递参数
JVM仍然会为匿名数组分配内存空间(堆中),当使用匿名数组的方法执行完毕后,JVM会自动回收它的内存空间
a=new int[] {1,3,5,7,9};
Java中允许长度为0的数组
比如编写一个结果为数组的方法,碰巧结果为空,可以构造一个长度为0的数组,这并不同于null
3.10.2 访问数组元素
数组元素编号从0开始
最后一个合法索引的值=数组长度-1
想要获得数组长度,即元素个数,可以直接调用array.length
想要打印数组中所有值,可以使用Arrays类的toString方法,这个方法会返回一个包含数组元素的字符串,这些元素会包括在[]中并被逗号分隔
例:
int[] a = {2,4,6,8,10};
String strArray = Arrays.toString(a);
//strArray="[2,4,6,8,10]"
当创建一个数值类型数组时,所有元素都会初始化为0;boolean型数组初始化为false;对象类型的数组则直接初始化为null
例如:
String[] names = new String[10];
创建一个可以包含10个字符串的数组,每个字符串当前都是null
如果希望是空串而不是null,则必须为元素提供空串:
for(int i =0;i<names.length;i++){
names[i] = "";
}
3.10.3 foreach 循环
foreach循环用来依次处理数组,或任何其他元素集合中的每一个元素,而不必去考虑索引值
Java中实现了这种循环,但是没有使用foreach关键字,直接是for关键字,具体格式为:
for(变量:集合)语句
这里的集合必须是一个数组或者实现了Iterable接口的类对象
比如:
int[] a = {2,4,6,8,10};
for(int elem :a){
System.out.println(e);
}
会输出a中的每一个元素
注意:foreach循环处理的是集合中所有元素,如果不想遍历全部元素,还是必须使用for循环
理论上foreach循环可以转换成for循环,但对于这种需要遍历集合中每一个元素的情况,foreach更加简洁,不需要关注起始和终止条件
3.10.4 数组拷贝
如果直接将一个数组变量赋值给另一个数组变量,会让两个数组变量指向同一个数组
int[] a = {1,2,3,4,5};
int[] b = a;
b[0] = 100;
//此时a[0] = 100,a和b实际上都是表示的同一个数组
如果确实希望将一个数组的所有值拷贝到一个新的数组中,应该使用Arrays类的copyOf方法
int[] a = {1,2,3,4,5};
int[] copya = Arrays.copyOf(a,a.length);
其中,第二个参数是新数组的长度。
这个操作可以用来扩大数组的大小,当新数组更长时:
- 如果新数组是数值型,新的元素会填入0
- 如果是boolean类型,会填入false
- 如果新数组更短,就只会拷贝前面的值
Java中的数组比起C中的数组,更类似于C中的数组指针
但是Java中没有指针运算,不饿能用数组名+1这种写法得到下一个元素
3.10.5 命令行参数
main方法中的String[] args参数,会接收一个字符串,就是命令行上指定的参数
public class TestArgsDemo {
public static void main(String[] args) {
if(args.length == 0 || args[0].equals("-h")) {
System.out.print("hello");
}else if(args[0].equals("-g")) {
System.out.print("goodbye");
}
for(int i = 1; i < args.length; i++) {
System.out.printf("%s",args[i]);
}
System.out.println("!");
}
}

注:
如果希望在idea中运行Java类时添加命令行参数,可以使用快捷键alt+shift+f10,选择编辑配置,打开后在应用程序的实参(Program arguments)字段中输入参数

以上可以看出,args里面的元素按照空格分隔保存每一个参数
Java中程序名并不存储在args数组中
3.10.6 数组排序
数组排序可以调用Arrays类的sort方法
int[] a = new int [100];
...
Arrays.sort(a);
这个方法使用了优化的快速排序算法,对于大多数数据集都很高效(这个方法没有返回值)
同时,可以使用binarySearch方法对数组查找:
static int binarySearch(xxx[]a,xxx v)
static int binarySearch(xxx[]a,int start,int end,xxx v)
这个方法会使用二分法查找a中是否有元素v,如果找到返回,返回元素的索引,否则返回一个负数r,-r-1是元素v按照原本顺序应该插入的位置
3.10.7 多维数组与不规则数组
多维数组使用多个索引访问一个数组元素,主要用于存储表格等较复杂的排列方式
例如,二维数组,也称矩阵的声明:
int[][] magicSquare = {
{1,2,3,4,5},
{6,7,8,9,10},
{11,12,13,14,15}
};
同样,每个维度的索引都是从0开始计数
但是实际上,Java并没有严格意义的二维数组,它的本质其实是一种元素为数组的一维数组
构造这种数组时并没有要求每一个数组的长度必须一致。
例如,存储这样的结构:
1,
1,2
1,2,3
1,2,3,4
声明这样的数组时首先需要明确指出行数
int[][] odds = new int[4];
之后,分别分配每一行
for(int row=0;row<4;row++){
for(int column=0;colum<odds[row].length;colum++){
odds[row][column] = colum+1;
}
}
完成创建后,可以直接利用索引来访问任意元素 odds[1][1]会输出第二行第二个元素
3.线性表
线性表:数据结构中最常用和最简单的一种结构。
3.1 线性表的定义
定义:线性表是零个多个数据元素的有限序列。
数学语言描述线性表:
若将线性表记为(a1,a2,…an),则表中ai-1领先于ai,ai领先于ai+1。
-
称ai-1是ai的直接前驱元素,ai+1是ai的直接后继元素;
-
当i=1,2,…n-1时,ai有且仅有一个直接后继;
-
当i=2,3,…n时,ai有且仅有一个直接前驱;
-
元素个数n>=0,称为线性表的长度,当n等于0时,称为空表
以上的定义可以看出:
- 线性表的元素是有限的,在计算机程序设计中无法讨论数学上无限的情况
- 线性表中头元素和尾元素比较特殊,因为它们的属性(只有一个前驱或只有一个后继)与别的元素有区别,但是计算机程序设计中循环是非常重要的一个步骤,因此在实现线性表时,对于头尾元素这样属性与一般元素有区别的元素,可能需要更多的考虑
- 作为一种序列,线性表中的元素之间存在顺序、
- 线性表中的元素种类应该是一致的,或者说,线性表用来描述同一类型的数据
- 就像一组学生名单,却加入了一个老师的信息,那么这个名单的设计就不够合理,也很难设计对这个表单的一系列操作
综合来看,非常多的实例都体现了线性表结构,比如花名册等表单类型数据、计算机程序中的数组、列表等
3.2 线性表的抽象数据类型
对于一种数据类型,不止要考虑数据本身的组织方式,还要考虑针对这种结构应该具有哪些操作
- 首先,任何一种数据结构,应该有初始化或者说,创建的方法
- 对应的,应该有针对这种数据结构的删除、清空操作
- 其次,作为元素的集合,应该有查询特定的元素是否存在的功能
- 对于一个集合,应该考虑如何向集合中增加或删除元素
- 由于线性表的结构是一对一的,且存在顺序,因此,能够自然想到根据元素次序获得元素的操作。
- 就像导游第一次带一队游客,导游不需要知道每个游客具体的姓名,只要游客排成一排,导游就能说“排在第5个的游客,请你买东西”之类的操作
线性表抽象数据类型定义:
ADT 线性表(List)
Data
线性表的数据对象集合为{a1,a2,...,an},每个元素的类型均为DataType。除第一个元素a1外,每一个元素有且仅有一个直接前驱元素;除最后一个元素an外,每个元素有且仅有1个直接后继元素。元素之间是1对1的关系
Operation
InitList(*L):初始化操作,建立一个空的线性表
ListEmpty(L):若线性表为空,返回true;否则返回false
ClearList(*L):将线性表清空
GetElem(L,i,*e):将线性表中的第i个元素值返回给e
LocateElem(L,e):在线性表L中查询与给定值e相等的元素。若查找陈功,返回该元素在表中的序号;否则返回0表示失败
ListInsert(*L,i,e):在线性表L中的第i个位置插入新元素e
ListDelete(*L,i,*e):删除线性表中第i个位置的元素,并用e返回删除的值
ListLength(L):返回线性表L的元素个数
EndADT
考虑完线性表的抽象数据类型后,就需要去思考线性表的具体实现,而计算机中,有两种存储结构,顺序存储和随机存储。无论选择哪一种,最重要的是将数据存储的同时存储数据之间的逻辑关系(可以理解为如何用程序设计语言实现抽象数据类型定义的功能)。
3.3 线性表的顺序存储结构
3.3.1线性存储定义
线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。
地址:存储器中,每个存储单元都有自己的编号,这个编号称为地址。
可以想象一栋每个房间一样大的公寓,每个房间都有自己的门牌号。
3.3.2顺序存储方式
为了实现顺序存储:
- 想象班主任带学生们旅游订酒店,那么:
- 酒店必须要知道一共要开多少房间 ——在内存中占据多大的空间,即最大存储容量
- 从哪个房间开始住学生(抽象讨论,不纠结现实情况) ——占据的空间的起始位置
- 也许有学生先到,实际入住当天要来多少人 ——线性表的长度(动态变化,不超出最大范围)
可以总结出,描述顺寻存储结构需要的三个属性:
- 存储空间的起始位置
- 线性表的最大存储容量
- 线性表的当前长度
根据以上特征,可以比较容易的考虑到,利用数组这一数据类型来实现线性表的顺序存储。数组非常符合上述的种种条件。
/**
* 文件名:sqlist.h
* 描述:对顺序线性表的抽象数据类型的实现
*/
#ifndef SQLIST_H
#define SQLIST_H
#include<stdio.h>
#include<stdlib.h>
#define OK 1
#define ERROR 0
#define MAXSIZE 20
#define true 1
#define false 0
typedef int Status; /*表示状态*/
typedef int ElemType; /*代表数据元素的具体值*/
typedef struct sqList
{
int data[MAXSIZE];
int listlength;
}sqList;
Status InitList (sqList** list); /*初始化操作,建立一个空的线性表 */
int ListEmpty(sqList list); /*若线性表为空,返回true,否则返回false */
Status ClearList(sqList* list); /*清空线性表 */
Status GetElem(sqList list,int localtion,ElemType* e); /*将线性表List中第localtion个元素值返回给e*/
int LocateElem(sqList list,ElemType e); /*查找List线性表中是否有e元素,如果成功返回e元素的位置,如果失败返回0*/
Status ListInsert(sqList* list,int localtion,ElemType e);/*在线性表List中的localtion位置插入元素e*/
Status ListDelete(sqList* list,int localtion,ElemType* e);/*删除线性表list中的第localtion个元素,并将值返回给e*/
int ListLength(sqList list); /*返回线性表L的元素个数*/
#endif
解释:
- c语言中没有boolean类型,虽然也可以用整数1和0表示真和假,但为了代码更加清晰,我还是定义了两个宏true和false
- 像插入、删除等函数相比返回值,更重要的是操作的实现,但我还是设计了用来表示状态的返回值,主要考虑到能够对失败的情况多一些处理
- 用数组来实现顺序存储的线性表,需要给定数组的最大长度,这个长度是固定的;而线性表的长度是动态变化的,表的长度应该<=数组最大范围
3.3.3地址计算方法与随机存取
根据顺序存储结构是连续空间的特点,又由于每个元素的类型一致,所以每个数据元素占据的空间大小也是相同的。
因此,能够得到:
设LOC(ai)为顺序存储结构线性表任意元素的地址,可以得到:
LOC(ai)=LOC(ai-1)+C = LOC(a1)+(i-1)*C
其中,C是每个数据元素占据存储空间的固定大小

而计算机程序对存储空间中的数据进行操作,都必须要知道这个数据的地址
跟具上述的公式,线性表中的任意元素,只要知道了首元素地址和元素固定大小,都能通过1次计算获得它的地址具体值
这种读取数据的操作,从时间复杂度的角度来看,时间复杂度为O(1)。
同时,这种存储结构也被称为随机存取结构。(因为允许通过索引访问任意元素不需要依次去读取,所以称为随机存取)
3.3.4顺序存储结构线性表的插入与删除
想象一群人按照次序排成一队,每个人占据的范围是固定的。
此时,若要在队伍中的任意一个位置加入一人,那么,如果加入这个人没有让表长超出上限,那么这个位置后的每一个人都必须要依次向后退一步。
由于插入每个位置的概率都是相等的,可以得知:
设X:向后退的人数(执行后退操作的步骤) ,X取值{1,2,3,…n-1}
E(X) = 1/nΣ(X) = n-1/2
时间复杂度 = O(n)
删除也是的思路,当去掉队伍中任意一个人或者说元素时,为了保证元素都能有直接前驱和直接后继,被删除的位置之后的元素,都需要向前一步,最终可以发现,时间复杂度也是O(n)。
3.3.5顺序存储结构线性表的具体C语言实现
#include "sqlist.h"
/**功能:创建一个空的顺序线性表,将创建好的线性表用List返回
* 参数:List:用于接收创建好的线性表
* 返回值:表示创建是否成功的状态
*/
Status InitList(sqList** list)
{
// printf("test in InitList Function\n");
*list = (sqList*)malloc(sizeof(sqList));
// printf("成功分配内存\n");
if(*list == NULL)
{
printf("内存分配失败");
return ERROR;
}
(*list)->listlength = 0;
return OK;
}
/**功能:若线性表为空返回true,否则返回false
* 参数: list:要判断的线性表
* 返回值: 若list为空返回true,否则返回false
*/
int ListEmpty(sqList list)
{
return list.listlength == 0 ? true:false;
}
/**功能:清空整个线性表
* 参数:list:要被清空的线性表
* 返回值:成功返回OK失败返回ERROR
*/
Status ClearList(sqList* list)
{
if(list == NULL)
{
printf("使用了空指针!");
return ERROR;
}
free(list);
list = NULL;
return OK;
}
/**功能:将线性表中第localtion个位置的元素返回给e
* 参数: list:查找的线性表
* localtion:需要返回的元素的位置
* e:接收被返回的元素
* 返回值: 成功返回OK,否则返回false
*/
Status GetElem(sqList list, int localtion, ElemType *e)
{
if(ListEmpty(list) == true)
{
printf("错误:列表为空!\n");
return ERROR;
}
if(localtion<1 || localtion>list.listlength)
{
printf("错误:查找的元素超出范围\n");
return ERROR;
}
*e = list.data[localtion-1];
return OK;
}
/**功能:查找list线性表中是否有e元素,如果有返回e所在的位置,如果没有返回false
* 参数:list:查找的线性表
* e:查找的目标元素
* 返回值:成功返回e所在位置,失败返回false
*/
int LocateElem(sqList list, ElemType e)
{
if(ListEmpty(list)==true)
{
printf("错误:线性表为空\n");
return false;
}
int length = list.listlength;
for(int i=0;i<length;i++)
{
if (e == list.data[i])
{
return i+1;
}
}
return false;
}
/**功能:在list线性表的localtion位置插入元素e
* 参数: list :要插入的线性表
* localtion: 要插入的位置,从1开始
* e: 要插入的元素
* 返回值:成功插入返回OK,否则返回ERROR
*/
Status ListInsert(sqList *list, int localtion, ElemType e)
{
if(list == NULL)
{
printf("错误:使用空指针\n");
return ERROR;
}
if(localtion<1 || localtion>list->listlength+1) //考虑如果插入末尾的情况允许location=listlength+1
{
printf("错误:试图在%d位置插入元素\n",localtion);
return ERROR;
}
if (list->listlength==MAXSIZE)
{
printf("错误:线性表已满\n");
return false;
}
// printf("location=%d,length=%d\n",localtion,list->listlength);
//只有不在最后一个位置插入元素时才对前面的元素进行移动,取向等是因为等于时正好插入到最后一个位置,也必须把原来位置的最后一个元素移走
if(localtion <= list->listlength)
{
for(int k =list->listlength-1;k>=localtion-1;k--) //保证localtion位置上原来的元素也被移动,所以这里是>=
{
list->data[k+1] = list->data[k];//向后移动数组
}
}
list->data[localtion-1] = e;
list->listlength++;
return OK;
}
/**功能:删除线性表list中localtion位置的元素
* 参数:list:要操作的线性表List
* localtion:要删除的元素的为hi
* e:接收被删除的元素
* 返回值:成功返回true,失败返回false
*/
Status ListDelete(sqList* list,int localtion,ElemType* e)
{
if(list == NULL)
{
printf("错误:使用空指针\n");
return ERROR;
}
if(ListEmpty(*list) == true)
{
printf("错误,删除空表\n");
return false;
}
if(localtion<1 || localtion>list->listlength)
{
printf("错误:操作位置越界\n");
return false;
}
*e = list->data[localtion-1];
if(localtion<list->listlength) //若不是最后一个元素
{
for(int i=localtion-1;i<list->listlength-1;i++)//向前移动数组
{
list->data[i] = list->data[i+1];
}
}
list->listlength--;
return OK;
}
/**功能:返回线性表元素的个数
* 参数;list:要被查询的线性表
* 返回值:线性表长度
*/
int ListLength(sqList list)
{
return list.listlength;
}
解释:
- 在插入和删除的函数中,我都事先进行了一个判断:只有要操作的元素不是列表最后一个元素,再进行移动元素的操作,可以避免最好情况发生时浪费资源
- 插入是所有元素向后移动,所以是把当前元素移动到后继元素的位置,插入的数组下标是从线性表末尾开始向前递减到(插入位置-1)
- 如果从插入位置开始递增的向后便利的话,当执行a[i+1] = a[i]之后,a[i+1]上原本的内容就被覆盖掉了
- 删除时所有元素向前移动,是后继元素向前移动,下标是从删除位置开始,递增向后直到列表末尾
3.3.6顺序存储结构线性表的优缺点
优点:
- 不需要为了实现数据结构的逻辑关系而增加额外的存储空间,物理空间本身就能反应元素之间的逻辑关系
- 可以快速的对任意位置的元素进行访问,实现随机存取
缺点:
- 插入和删除需要移动大量的元素
- 当线性表长度变化较快时,难以确定存储空间的容量
3.4 线性表的链式存储结构
顺序存储的最大缺点就是,对于每一次插入或者删除操作,都必须要经历O(n)的操作
这是因为顺序存储必须保证元素之间物理位置是连续的,才能实现数据结构的逻辑关系
为了解决这个问题,可以采用链式存储结构。
3.4.1线性表链式存储结构定义
链式存储结构中,每个元素的物理地址是随机的,可以连续,也可以不连续
就像餐馆的拿号排队,只要你拿到了排队号,人就可以随意走动,你也能知道什么时候该你进餐厅吃饭
可见,对于餐馆排队这个模型,最重要的就是得拿到一个表示顺序的排队号码
抽象到数据元素层面,为了实现链式存储结构,每个数据元素除了存储本身的值的数据域以外,还需要一个存储直接后继元素信息的指针域。
定义:n个结点(结点=元素值+指针域信息)连接成一个链表,即为线性表(a1,a2,…,an)的链式存储结构
每个结点只有一个指针域,所以也称为单链表
头指针与头结点
头指针:链表中第一个结点的存储位置
规定线性链表的最后一个元素的指针域为空
头结点:设计中,可以在线性表的第一个元素之间再增加一个结点,称为头结点。
头结点中并不存放线性表的具体信息,只是为了操作上的便捷,头结点的指针域指向线性表的第一个元素
| 头指针 | 头结点 |
|---|---|
| 一个指针变量 | 一个结点(c语言中的结构体) |
| 指向链表的第一个结点,如果链表有头结点,就指向头结点 | 指针域指向链表第一个元素 |
| 用来标志整个链表 | 主要用来简化插入和删除等操作 |
| 是链表必须的 | 不是必须的 |

linklist listPtr = (linklist)malloc(sizeof(Node));
这可以是头指针 这是头结点
3.4.2 链式存储线性表的代码描述
指针变量
实现链表的最关键在于指针变量。指针变量也是一种变量,存储的值是存储单元的地址(通常是一个十六进制数),指针变量存的值是哪个变量的地址,就代表了指向这个变量。
指针变量的类型格式可以简单的理解为 基本数据类型+*,如:
int* char* struct Node * .....
可以牢记一个简单的原则:凡是希望在函数中修改变量的值,我们就传递指针变量
单链表的C语言实现:
#ifndef LINKLIST_H
#define LINKLIST_H
#include <stdio.h>
#include<stdlib.h>
#define OK 1
#define ERROR 0
#define HEAD_NUMBER -1
#define true 1
#define false 0
typedef int ElemType;
typedef int Status;
typedef struct Node
{
ElemType data;
struct Node* next;
}Node;
typedef struct Node* LinkList;
Status InitList (LinkList* doubleListPtr); /*初始化操作,建立一个空的线性表 */
int ListEmpty(LinkList listPtr); /*若线性表为空,返回true,否则返回false */
Status ClearList(LinkList* doubleListPtr); /*清空线性表 */
Status GetElem(LinkList listPtr,int localtion,ElemType* e); /*将线性表List中第localtion个元素值返回给e*/
int LocateElem(LinkList listPtr,ElemType e); /*查找List线性表中是否有e元素,如果成功返回e元素的位置,如果失败返回0*/
Status ListInsert(LinkList* doubleListPtr,int localtion,ElemType e);/*在线性表List中的localtion位置插入元素e*/
Status ListDelete(LinkList* doubleListPtr,int localtion,ElemType* e);/*删除线性表list中的第localtion个元素,并将值返回给e*/
int ListLength(LinkList list); /*返回线性表L的元素个数*/
#endif
解释:
- 在初始化、插入、删除等函数,传递的参数都是LinkList* 类型,相当于Node的双重指针。可以这么理解:
- 在这些函数中,我们希望,指向链表在函数中被修改(粗略的把指针LinkList理解成代表链表),所以传递它的指针类型
链表的读取与遍历
不同于顺序结构,程序可以通过一次计算就能得到任意元素的具体地址,为了找到链式存储结构中的任一元素,我们只能依靠它的逻辑关系,依次遍历的去访问每一个元素,直到目标元素
设X:到目标元素需要遍历的次数,X取值{1,2,…n-1},遍历每个位置都是等可能的
所以,E(X) = 1/nΣX = n-1/2 => 时间复杂度取O(n)
具体实现:
/**
* 功能:获取第localtion个元素,并将其指传递给e
* 参数:listPtr:要查询的线性表
* localtion:要查询的元素的位置 从1开始
* e:接收查到的结果
* 返回值:成功返回OK,失败返回ERROR
*/
Status GetElem(LinkList listPtr,int localtion,ElemType* e){
if (ListEmpty(listPtr)==true)
{
printf("错误:线性表为空\n");
return ERROR;
}
LinkList workPtr = listPtr;
int pointOfworkPtr = 0;
while (workPtr != NULL && pointOfworkPtr < localtion) //从头结点开始遍历,直到到达定位的位置或者workPtr为空时退出循环
{
workPtr = workPtr->next;
pointOfworkPtr++;
}
if(workPtr == NULL) //如果workPtr =null 证明移动到了链表的末尾的next域,否则即使localtion指定链表最后一个元素workPtr也停在最后一个元素
{
return ERROR;
}
*e = workPtr->data;
return OK;
}
假设链表中有三个元素和一个头结点,我们去查询第四个位置:

while(workPtr!=null && pointOfworkPtr<localtion)
localtion = 4
| 开始前 | 第一次 | 第二次 | 第三次 | 第四次 | |
|---|---|---|---|---|---|
| workPtr指向 | 头结点 | a1 | a2 | a3 | ^ |
| pointOfworkPtr | 0 | 1 | 2 | 3 | 4 |

if(workPtr==null)
当第四次循环结束,不满足循环条件,此时退出循环,且workPtr == null,函数成功返回ERROR
int main(){
LinkList list = NULL;
ElemType e = 3;
int result=0;
LinkList printPre;
if (InitList(&list)==OK)
{
printf("初始化成功\n");
printList(list);
}
for(int i =0;i<3;i++)
{
ListInsert(&list,i+1,i);
}
printf("插入3次后,线性表结果:\n");
printList(list);
printf("#########\n");
result = GetElem(list,4,&e);
if (result == OK)
{
printf("第4个元素是%d\n",e);
}else{
printf("查询失败");
}
free(list);
}
考虑当链表为空而试图查询位置1的情况:

while(workPtr!=null && pointOfworkPtr<localtion)
localtion =1
| 初始化 | 第一次 | |
|---|---|---|
| workPtr | 头结点 | ^ |
| pointOfworkPtr | 0 | 1 |
![]() |

if(workPtr==null)
当执行一次循环体后,workPtr=NULL,退出循环,函数正常返回ERROR
int main(){
LinkList list ;
int result = InitList(&list);
if (result == OK)
{
printf("初始化成功\n");
}
int e = 0;
result = GetElem(list,1,&e);
if (result == OK)
{
printf("第1个元素是%d\n",e);
}else{
printf("查询失败!");
}
}

可见,正是由于头结点的存在,我们用相同的循环控制条件和越界判断条件,都实现了对整个链表的控制,无需额外去思考链表为空的情况。
在判断链表是否为空时,只需要统一的if(listPtr->next == null)就能实现判断的逻辑。
3.4.3 链式存储结构线性表的插入与删除
对于链表这种链式存储结构,无论是插入、删除还是查询,凡是要对具体位置进行操作的,都需要类似的遍历步骤到达需要的位置
由此,也体现除了头节点的优势:在遍历的过程中,并不需要对空链表的情况做出额外的处理,用统一的设计即可完成对链表的操作。
否则,都需要在操作开始前,具体分析链表是否为空的情况。
插入
-
当工作指针到达指定位置时:
-
将插入位置的后继信息交给新的节点
-
让插入位置的前驱指向新的节点

-
要注意的是这个顺序一定不能取反,否则,如果直接让前驱元素指向新的结点,那么原本的所有后继元素都会丢失了。
(因为所有元素都只有一个后继,先让前面的元素后继变成了新节点,那么原本后继信息自然就被覆盖了)
/**
* 功能:在线性表的第localtion位置插入元素e
* 参数:dobuleListPtr:指向要插入的线性表指针的指针
* localtion:要插入的元素的位置,从1开始
* e:要插入的元素
* 返回:成功返回OK,否则返回ERROR
*/
Status ListInsert(LinkList* dobuleListPtr,int localtion,ElemType e){
if (dobuleListPtr == NULL)
{
printf("错误:传递了空指针\n");
return ERROR;
}
// printf("进入插入函数\n");
LinkList listPtr = *dobuleListPtr;
LinkList workPtr = listPtr; //从头节点开始遍历
int pointOfworkPtr = 0; //代表了workPtr的位置,由于头结点认为是0位置,所以从0开始
/*这里workPtr去判断而不是workPtr->判断,实现了允许在已有位置后面一位插入*/
while (workPtr != NULL && pointOfworkPtr< localtion-1) //workPtr移动到localtion-1的位置,因为是要在localtion位置插入数据而不是localtion后插入数据
{
workPtr = workPtr->next; //移动workPtr
pointOfworkPtr++;
}
if (workPtr == NULL )//由于头结点的存在,所以即使插入第一个位置也不会有区别
{
printf("错误,位置超过长度\n");
return ERROR;
}
LinkList newNode = (LinkList)malloc(sizeof(Node));
newNode->data= e;
newNode->next= workPtr->next; //首先让新节点连接上插入位置前一个元素的后继
workPtr->next = newNode;
return OK;
}
解释:
-
实际上这里遍历让workPtr指向的位置是要插入位置的前一个元素(pointOfworkPtr<localtion-1),因为要求是实现插入到localtion位置,如果希望的功能是插入到localtion之后的位置,那么应该让workPtr指向要插入位置的元素
-
这里的越界判断是workPtr==null,因此,实际上,可以在线性表实际长度之后(即最后一个元素之后)的第一个位置插入元素,具体分析如下:
-
假设线性表中有3个元素,而我们试图在第4个位置插入新结点:
-

-
while(workPtr != null && pointOfworkPtr<localtion-1)
-
localtion=4
-
初始化 第一次 第二次 第三次 workPtr 头结点 a1 a2 a3 pointOfworkPtr 0 1 2 3 -

-
当第三次循环结束时,workPtr=a3,并不会触发越界条件(if(workPtr==null)),可以用同样的代码实现插入操作
-
但是,如果试图在list.length+1之后的范围插入元素,就会因为workPtr==null触发越界,让程序按照逻辑的返回ERROR
-
-
这样的操作不符合设计初衷(插入链表中任意位置,在末尾之后的一个位置理论上不属于线性表),但是比较符合直觉
-
如果要严格遵守设计,应该让越界条件置为if(workPtr->next != null)
删除
由于链式存储结构的所有结点都是由malloc()函数申请的,因此必须使用free()函数释放内存
即使试图删除整个链表,光是删除头结点或第一个结点,也并没有意义,因为头结点(或第一个元素)之后的结点占据的空间仍然存在,它们并不会自动的被释放。
一般的变量存储在栈中,随着进程的结束,会被自动销毁。
但是malloc函数开辟的空间在堆上,并不会消失
现代操作系统(windows、Linux等)会在程序终止后,自动回收该进程分配的所有内存,包括:
- 堆上的malloc()分配的内存
- 进程的栈空间
- 代码段、数据段等
这对于短时间运行的程序是有意义的,但是,有一些程序会长期运行(比如服务器),对于这一类程序,如果不用free()函数去释放内存,就会导致这一片内存无法使用,造成内存泄漏。不仅可能降低效率,导致系统内存不足,还可能让操作系统意外的自动终止进程(linux的OOM killer机制)
因此,应该养成一种习惯,不管程序运行环境是否有自动垃圾回收等系统,凡是动态开辟内存,都应该对内存的最终释放保持警惕。
具体到对于链表中任一个结点的删除:
- 首先,需要遍历到需要删除的位置
- 之后,将删除位置的后继交给它的前驱元素,保证信息不会丢失
- 最后,对要删除的结点调用free()

/**
* 功能:删除第location个位置的元素
* 参数:doubleListPtr:指向要删除元素的线性表的指针
* localtion:要删除的元素的位置
* e:用来接收要删除的元素
* 返回值:成功返回OK,失败返回ERROR
*/
Status ListDelete(LinkList* doubleListPtr,int localtion,ElemType* e){
if (doubleListPtr == NULL)
{
printf("错误,传入了空指针\n");
return ERROR;
}
if (localtion < 1)
{
printf("错误,试图删除小于1的位置\n");
return ERROR;
}
LinkList listPtr = *doubleListPtr;
LinkList deletePtr,frountPtr;
frountPtr = listPtr;
int pointOffrountPtr = 0; //从头节点开始
//移动到要删除的元素的前一个元素位置
while (deletePtr != NULL && pointOffrountPtr < localtion-1)
{
frountPtr = frountPtr->next;
pointOffrountPtr++;
}
//删除的位置超出了最后一位置的元素
if (frountPtr == NULL)
{
return ERROR;
}
deletePtr = frountPtr->next; //指向真正要删除的元素
frountPtr->next = deletePtr->next; //将要删除的元素的后继交给它的前驱
*e = deletePtr->data;
free(deletePtr);
return OK;
}
解释:
- 和插入操作类似,要求是删除位于localtion位置的元素,因此让工作指针遍历到删除元素之前一个的位置(实际代码中我的工作指针命名为frontPtr)
- 由于存在头结点,即使删除第一个元素也不会有任何区别,使用同一套代码即可,否则,还需要单独讨论删除第一个元素的情况
3.5 单链表结构与顺序存储结构的区别
由于每一次插入删除之前,都需要对工作指针进行遍历,所以,如果只进行一次插入或删除操作,单链表的时间复杂度仍然是O(n)
但是,假设不止插入1个元素,而是m个元素到任意位置:
- 对于顺序存储结构,每一次插入或删除,都要n-1/2次移动元素的操作,那么总的时间复杂度就是O(m*n)
- 对于链式存储结构,只要遍历到了需要的位置,那么无论插入(删除)多少个元素,时间复杂度都是O(1),总的来说,时间复杂度保持在O(n)
由此,可以得出以下结论:
- 如果是需要频繁查询(读写)操作,而很少需要插入和删除的线性表,应该考虑顺序存储结构。
- 比如用户注册的个人信息,很少需要对个人信息加一项或减少一项,而是修改或读取的操作很多
- 如果是需要大量插入和删除的线性表,应该使用链式存储。
- 比如购物车购物清单(假设用线性表实现),需要用户多次的增加或减少数据元素
- 对于元素个数变化比较大或者不明知元素的个数时,因该使用链式存储。
正则表达式简介
一.正则表达式简介
正则表达式也是一种工具,用来解决特定的问题
正则表达式是一种特殊的字符串,用来匹配和处理文本核心作用是:
- 查找特定的信息
- 查找到信息后替换
正则表达式的语法并不是很复杂,但更困难的问题是该如何构造足够正确的表达式尽量满足需求
应用程序大多通过菜单选项和对话框来使用正则表达式
程序设计语言一般利用函数、类等提供正则表达式的功能
二.匹配单个字符
1.直接匹配
最简单的正则表达式就是你需要什么文本就直接构造什么要的正则表达式
例子:
- 查询文本:“Hello,my name is Ben,my father’s name also is Ben.”
- 正则表达式,也称为模式:Ben
- 结果:“Hello,my name is Ben,my father’s name also is Ben.”
正则表达式会循环的匹配文本中所有字符,但一般都默认返回第一个匹配的文本
不过大部分程序设计语言都实现了返回所有匹配结果的方式,比如JavaScript中的g(global),会返回一个包含所有匹配结果的数组
正则表达式区分大小写,所以Ben无法匹配ben或BEN
2.匹配任意单个字符
利用正则表达式中的一种特殊字符,也称为元字符——.这个字符代表匹配任意的字符(不包括空白符)
例子:
- 文本:“sal1.xls sat.xls sbl2.xls scl.xls”
- 模式:s.l
- 结果:“sal1.xls sat.xls sbl2.xls scl.xls”
- 解释:
- 正则表达式不会匹配完整的字符串,只会匹配符合要求的部分
- 这里使用的模式意思是:以s开头+任何字符+l结束的字符串
如果希望匹配“.”等元字符本身,需要在模式中使用转义符\。
三.匹配一组字符
1.匹配多个字符中的某一个
使用[]来包括我们希望匹配的摸一个字符可能出现的结果,[]称为字符集合
例子:
- 文本:“Java is good,javascript is good too.”
- 模式:[Jj]ava
- 结果:“Java is good,javascript is good too.”
- 解释:
- 利用[Jj]表示希望匹配到Java或者java字符串
- 字符集合中是一种或者的关系,并不需要匹配每一个
2.范围匹配
利用这种字符集合,我们可以针对一段范围内的字符进行匹配
例如:
- [0-9] 就等价于 [0123456789] 表示匹配任意一个数字
- [a-z]匹配ASCⅡ中在a到z之间任意一个字符
- 同理:[A-Z]匹配ASCⅡ中在A到Z之间任意一个字符
- 注:一般不用[A-z]匹配任意字母,因为在ASCⅡ中,从A到z不止有字母,还包含了一些其他字符
3.排除
有时候我们不希望正向的去构造应该匹配哪些字符,而是希望反过来不匹配哪些字符
语法:
[^要排除的字符范围]
比如:[^a-zA-Z0-9]表示匹配一个不在0-9、a-z、A-Z范围的字符
注意:^是一个元字符,它只有在字符集合中且处于最左边的时候才代表排除,如果在不同的位置,这个元字符会代表其他含义
四.元字符
正则表达式中的元字符有些类似printf方法中的格式控制符,它们会发挥特殊的作用,如果希望使用它们本身,就需要对他们进行转义
常用的元字符:
| 元字符 | 说明 |
|---|---|
| \f | 匹配换页符 |
| \n | 匹配换行符 |
| \r | 匹配回车符 |
| \t | 匹配制表符 |
| \s | 匹配任何一个空白符(包括上面所述) |
| \S | 匹配任何一个不能被\s匹配的字符 |
| \d | 匹配任何一个数字字符(0到9) |
| \D | 匹配任何一个非数字字符,等价于[^0-9] |
| \w | 匹配任何一个字母(大小写都有)、数字或者下划线 |
| \W | 匹配任何一个不能被\w匹配的字符 |
注意,元字符区分大小写,一般大写的和小写的意思取相反
例子:
- 文本:”12345 123456 a1b2c3 AABCE 7788CE“
- 模式:\w\d\w\d\w\d
- 结果:”12345 123456 a1b2c3 AABCE 7788CE“
五.重复匹配
1.指定匹配次数
前面提到的模式实际上都只能匹配一次
假设我们希望匹配邮箱格式,比如123456GG@example.com
设计模式 :\w@\w\.com
最终,我们什么也匹配不到。因为按照上面设计的模式,它实际能匹配到的内容只能是 a@b.com这种格式,\w只会匹配一个任意符合标准的字符
如果希望匹配多次,可以使用{至少匹配次数,至多匹配次数}跟在需要多次匹配的正则表达式后面,指定匹配次数
如\w{1,3}会匹配1到3次(也就是1到3个)符合标准的字符
而{1,}格式表示匹配至少一次;{,5}表示最多匹配5次,最少可以是0次。
或者也可以明确指定希望匹配多少次,比如[0-9]{6}表示匹配一个数字字符6次
注意指定的重复匹配次数只作用于紧跟的正则表达式,不会影响到其他字符
例子:
文本:body{
background-color: #fefbd8;
}
模式:#[A-Fa-f0-9]{6}
结果:body{
background-color: #fefbd8;
}
这里利用正则表达式成功的从CSS样式表中选择到了颜色的具体编号
2.表示匹配次数的元字符
有一些特殊的元字符用来简化匹配次数
+: 表示匹配至少1次,等价于{1,}
*:表示匹配0次或多次,等价于{0,}
?:表示匹配0次或1次,等价于{0,1}
利用重复匹配,我们可以实现选择性的匹配字符
比如linux操作系统中换行符是\n,而winodws系统中换行符是\r\n
我们不确定收到的文本来自linux系统还是winodws系统,但为了使用一种模式来匹配不同的情况,可以这么设计
模式:\r?\n
表示换行符\n之前的\r可以不出现,也可以出现,最多出现0次。
注:这些元字符被称为量词,这些量词在默认情况下,是贪心的,也就是说,它们会尽可能的匹配满足的结果
比如:
文本:<p>这是一段文本</p> <p>这是另一段文本</p>
我们希望分别匹配每一段p标签的内容,设计模式:
<[Pp]>.*</[Pp]>
最终我们会得到的结果是:
<p>这是一段文本</p> <p>这是另一段文本</p>
可以发现,匹配结果超出了我们的预期,正是因为*会尽可能多的匹配满足.的字符,把所有在<p>和</p>之间的文本都匹配了
如果我们希望尽可能少的满足匹配,应该使用懒惰型量词
| 贪婪型量词 | 懒惰型量词 |
|---|---|
| * | *? |
| + | +? |
| {n,} | {n,}? |
例如,修改上面的模式:
<[Pp]>.*?</[Pp]>
得到的结果就会是:
<p>这是一段文本</p> <p>这是另一段文本</p>
六.位置匹配
有时,我们不仅需要匹配字符的内容,还需要匹配字符的位置
比如:用户输入了一段文本:
The cat scattered his food all over the room.
我们希望匹配到所有cat单词
模式:cat
最终我们得到的结果会是:The cat scattered his food all over the room.
为了实现我们希望的结果,我们需要限制匹配的范围,或者说,去匹配字符串的位置
1.单词边界
使用\b匹配单词的边界,意思是这个元字符并不是匹配任何字符,而是会匹配一种位置,这个位置的一边是任意能被\w匹配的字符,一边是能被\s匹配的字符
我们修改模式 :\bcat\b
这里两个\b分别制定了两个单词边界
最终得到结果:The cat scattered his food all over the room.
类似其他元字符,\B可以匹配不能被\b匹配的位置
利用\B,我们可以检查多余的空格
比如 文本:abc efg c mm
这里c与其余字符之间隔了两个空格
我们使用模式:\Bc\B
得到结果:abc efg c mm
2.字符串边界
单词边界用于对单词的位置进行匹配,字符串边界类似,但是作用范围是整个字符串,或者说整个文本
^匹配字符串的开始位置
$匹配字符串的结束位置
例子:用户输入了一份文本,我们希望检查是否是xml标准文本
XML文档规定,起始标签必须是<?xml> 且位于文档开头
假设用户输入文本内容:
六.位置匹配
有时,我们不仅需要匹配字符的内容,还需要匹配字符的位置
比如:用户输入了一段文本:
The cat scattered his food all over the room.
我们希望匹配到所有cat单词
模式:cat
最终我们得到的结果会是:The cat scattered his food all over the room.
为了实现我们希望的结果,我们需要限制匹配的范围,或者说,去匹配字符串的位置
1.单词边界
使用\b匹配单词的边界,意思是这个元字符并不是匹配任何字符,而是会匹配一种位置,这个位置的一边是任意能被\w匹配的字符,一边是能被\s匹配的字符
我们修改模式 :\bcat\b
这里两个\b分别制定了两个单词边界
最终得到结果:The cat scattered his food all over the room.
类似其他元字符,\B可以匹配不能被\b匹配的位置
利用\B,我们可以检查多余的空格
比如 文本:abc efg c mm
这里c与其余字符之间隔了两个空格
我们使用模式:\Bc\B
得到结果:abc efg c mm
2.字符串边界
单词边界用于对单词的位置进行匹配,字符串边界类似,但是作用范围是整个字符串,或者说整个文本
^匹配字符串的开始位置
$匹配字符串的结束位置
例子:用户输入了一份文本,我们希望检查是否是xml标准文本
XML文档规定,起始标签必须是<?xml> 且位于文档开头
假设用户输入文本内容:
this is wrong xml text
<?xml version=“1.0” encoding=“UTF-8”>
……
这很显然不符合XML标准
如果我们设计模式:<?xml>
会匹配到:
this is wrong xml text
<?xml version=“1.0” encoding=“UTF-8”>
……
会误认为这是一段合法的XML文档,将其当作XML文档处理,就有可能产生很多错误
因此,修改我们的模式,使用字符串边界匹配:
^<?xml>
这样就不会误将错误的文本匹配成XML文档
七.子表达式
前面的重复次数的元字符都只能作用于最近的正则表达式,如果希望作用于一大部分正则表达式,可以使用括号包裹一部分正则表达式,使其形成一种整体,称为子表达式
例如:文本: I love you
我们希望去检查空格,也就是
模式: +
最终并不能匹配到任何内容,因为+只会作用于离他最近的字符;,我们这个模式最终能匹配的字符形式应该是 ;;; 这样的模式
修改为:( )+
能达到我们的需求
子表达式可以嵌套使用
八.反向引用
1.利用反向应用匹配
使用子表达式,不仅能对正则表达式进行分组,使用一个整体,还可以实现对正则表达式的反向引用
例如:
一段文本:”I am am a student of of the school and and I hate study.”
如果我们试图找出所有写重复的单词,利用之前的正则表达式无论如何也做不到
因为想找到重复的单词就意味着正则表达式必须知道前面的内容到底是什么,才能使用这一部分
设计模式:[ ]+(\w+)[ ]+\1
结果:I am am a student of of the school and and I hate study.
这里[ ]+表示匹配一个或多个空格;
\w+表示匹配1个或多个分组,并且属于一个子表达式
之后又是[ ]+匹配一个或多个空格
最后\1就是所谓的反向引用,这里1表示引用第一个子表达式匹配的内容,以此类推2能代表第二个子表达式。。。
可以 把反向应用理解成一种变量
注意:Java中使用$标识反向引用的编号
例子:
一段HTML文本中,我们希望匹配到从1级到6级的标签<h1>、<h2>….
<body>
<h1>这是1级标题</h1>
<h2>这是2级标题</h2>
<h3>这是3级标题</h3>
</body>
使用模式:
<[Hh]([0-6])>.?*</[Hh]\1>
结果:
<body>
<h1>这是1级标题</h1>
<h2>这是2级标题</h2>
<h3>这是3级标题</h3>
</body>
解释:
- 首先使用<>直接匹配这两个字符,<>中的[Hh]表示匹配大写或小写的h,之后在子表达式中任意匹配一个1~6的字符
- 之后在结束标签中不直接规定h后面跟什么数字,而是使用反向引用,保证了针对不同级别的标签,都能获得与它完全同级别的结束标签
2.利用反向引用进行替换
使用反向匹配机制,我们能够使用正则表达式,自动的完成对文本的替换
想实现替换操作,我们需要设计两种正则表达式,一种用来寻找匹配的目标文本,一种用来指定要替换的新文本
例如:
我们希望在html中找到邮箱格式,并把它用a标签修改成能直接点击的超链接
this is my email 123456@example.com
模式:
-
匹配模式:(\w+@\w+\.com)
-
替换模式:<a href=“emali:$1”>$1</a>
-
最终结果:
-
this is my email <a href="email:123456@example.com">123456@example.com</a>
九.环视
环视可以理解为一种条件,表示环视的正则表达式也会去匹配字符,但匹配到符合条件的字符后并不会返回这些值
相当于给自己的正则表达式上了一种条件,有能匹配环视字符的字符才继续匹配
环视分为向前查看和向后查看
1.向前查看
向前查看的语法是
本来要匹配的内容(?=要满足的条件)
例子:“https://baidu.com http://example.com ftp://ftp.forta.com”
我们希望从一系列的URL种匹配出协议名,又不希望包含:
模式:.+(?=:)
结果:“https://baidu.com http:/example.com ftp:/ftp.forta.com”
解释:
- .+匹配任意字符1次以上,同时需要满足在.+的字符串后面要有一个字符:
- 从:字符的角度相对来说,就像从:向前面去匹配了,所以称为向前查看
- 同时这种虽然匹配但不返回结果的行为,也称为对文本的无消耗
2.向后查看
类似于向前查看,主要是查看已经匹配文本之前的内容
语法:
(?>=条件)已经匹配的字符串
例如,一份购物清单:
item1:$152.4
item2:$990.33
item3:$500.0
我们希望匹配到物品的具体金额同时不希望包含美元符号
可以使用向后查看来进行准确定位:
模式:(?>=\$)[\d\.]+
这里向后查看限制了我们之后返回内容的模式只能去匹配开头有一个美元符号的字符。
十.Java中使用正则表达式
Java中主要使用java.util.regex包中的Pattern类和Matcher类来支持正则表达式的处理。
Pattern主要用于编辑正则表达式
而Matcher类主要用于充当匹配器的逻辑
1.匹配字符串
例:
import java.util.regex.*;
public class RegexExample1 {
public static void main(String[] args) {
String text = "hello123";
String regex = "\\w+\\d+"; // 匹配字母+数字的组合
Pattern pattern = Pattern.compile(regex); // 编译正则表达式
Matcher matcher = pattern.matcher(text); // 创建匹配器
if (matcher.matches()) {
System.out.println("匹配成功!");
} else {
System.out.println("匹配失败!");
}
}
}
注意:在Java中编写正则表达式时,对于元字符必须要使用\\,因为需要第一个转义符来转移元字符本身自带的\。
2.替换字符串
例:
import java.util.regex.*;
public class RegexExample2 {
public static void main(String[] args) {
String text = "邮箱1:user1@example.com,邮箱2:user2@test.com";
String regex = "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"; // 匹配Email
String result = text.replaceAll(regex, "[邮箱已隐藏]");
System.out.println(result);
}
}
这例匹配字符串的模式稍微有些复杂,主要是考虑到有些邮箱地址可能是abc.edf@example.somthing.com之类的格式
3.Java中使用正则表达式的意义
在 Java 后端开发中,正则表达式具有广泛的应用:
- 数据校验:
- 验证 邮箱、手机号、身份证号、密码强度 等。
- 文本处理:
- 处理 日志文件、HTML/XML 解析、过滤敏感词 等。
- 字符串提取:
- 从文本中提取 URL、IP 地址、日期 等特定格式数据。
- 日志分析和搜索:
- 在 日志系统 或 监控系统 中,用正则匹配 错误信息,便于分析问题。
在高效处理结构化文本、数据过滤和安全控制方面,正则表达式是一种 简洁、强大 的工具,在 Java 后端开发中至关重要。
但是也要注意,本身正则表达式只是一种工具,也有自己的缺陷。
比如:
-
面对比较复杂的语法(嵌套的HTML、XML、JSON)时正则表达式很难解析。
-
处理超长字符串或大数据时光靠正则表达式效率也会很低
-
正则表达式本身比较晦涩,会降低自己的代码可读性,可能的情况下,我们都应该优先考虑字符串处理方法
总而言之,我们应该利用正则表达式,而不是反过来被这种工具消耗大量的精力。
4.计算机网络
1.4 与HTTP关系密切的协议:IP、TCP、DNS
IP协议
IP协议简介
IP:Inter Protocol,网际协议。几乎所有用到网络通讯的设备都会用到IP协议
IP协议的主要作用就是把各种数据包传给对方
IP协议的实现主要依靠两个重要条件:IP地址与MAC地址。
节点 Node:
- 计算机网络中,节点时连接到网络并能发送、接收或转发数据的设备或连接点。
- 包括:
- 计算机:如个人电脑、服务器等。
- 网络设备:如路由器、交换机、集线器等。
- 打印机:网络打印机等外围设备。
- 移动设备:如智能手机、平板电脑等。
- 物联网设备:如智能家居设备、传感器等。
- 节点的主要功能:
- 数据发送与接收:生成、发送或接收数据。
- 数据转发:如路由器、交换机等设备负责转发数据包。
- 网络管理:某些节点负责监控和管理网络流量。
- 总结来说,节点是网络中的基本单元,负责数据的接收、发送和转发
而IP地址与MAC地址都是对于节点的唯一标识。
两者可用于标识同一网络设备,但是二者在网络通信中扮演不同的角色,既有联系也有区别。
联系
- 共同作用:两者都用于在网络中唯一标识设备。
- 协同工作:在网络通信中,IP地址和MAC地址通常一起使用。IP地址用于网络层寻址,而MAC地址用于数据链路层寻址。
区别
- 层次不同:
- IP地址:工作在网络层(OSI模型的第三层),用于在不同网络之间寻址和路由数据包。
- MAC地址:工作在数据链路层(OSI模型的第二层),用于在同一局域网(LAN)内寻址和传输数据帧。
- 地址格式:
- IP地址:通常为IPv4(如192.168.1.1)或IPv6(如2001:0db8:85a3::8a2e:0370:7334)格式。
- MAC地址:通常为48位,表示为12个十六进制字符(如00:1A:2B:3C:4D:5E)。
- 分配方式:
- IP地址:可以动态分配(如通过DHCP)或静态配置,且可以更改。
- MAC地址:由设备制造商固化在硬件中,通常不可更改。
- 作用范围:
- IP地址:用于全球范围内的网络通信,跨越不同的网络。
- MAC地址:主要用于局域网内的通信,不跨越不同的网络。
- 协议依赖:
-
IP地址:依赖于IP协议(如IPv4、IPv6)。
-
MAC地址:依赖于以太网协议或其他数据链路层协议。
一般来讲,IP地址可以动态变化,而MAC地址保持不变
这是因为:
- IP地址是一种逻辑标识,当网络重组、迁移或重新配置时,能够该变化的IP地址能够灵活的适应新的网络拓扑
- MAC地址主要作用范围是局域网内,固定的MAC地址能保证信息在局域网内稳定准确的传输
- ARP地址解析协议依赖于固定的MAC地址的唯一性,来映射IP地址和MAC地址
IP地址与MAC地址协同工作的模型
假设两个处于同一局域网的设备:
- A(IP地址:192.168.1.2,MAC地址:00:1A:2B:3C:4D:5E)
- B(P地址:192.168.1.3,MAC地址:00:1A:2B:3C:4D:6F)
- 由A向B发送数据
大致经历以下步骤:
- 确定目标IP:通信进入网络层之前A会获得B的目标地址
- A确定B在同一局域网中,于是发送ARP请求广播到局域网,类似于询问“谁的IP地址是192.168.1.3,请告诉我你的MAC地址”
- B收到ARP请求后,识别出自己的IP,于是向A发送一个ARP响应,包含自己的MAC地址
- A收到B的响应后,开始构建数据帧,数据帧包括:
- 目标MAC地址:00:1A:2B:3C:4D:7F(默认网关的MAC地址)
- 源MAC地址:00:1A:2B:3C:4D:5E(设备A的MAC地址)
- 目标IP地址:203.0.113.5(设备B的IP地址)
- 源IP地址:192.168.1.2(设备A的IP地址)
- 数据:实际要发送的数据
- A将构建好的数据帧发送到局域网中
- B接收数据帧,检查目标MAC地址是否与自己的符合,如果符合,进行相应的处理

假设不在同一局域网中通信:
A(IP地址:192.168.1.2,MAC地址:00:1A:2B:3C:4D:5E)
B(IP地址:203.0.113.5,MAC地址:00:1A:2B:3C:4D:6F)
A位于局域网(192.168.1.0/24)
B位于局域网(203.0.113.0/24)
两个局域网通过路由器连接
步骤:
- A获得目标IP地址 203.0.113.5
- A通过子网掩码(如225.225.225.0)判断目标IP地址不在同一个局域网中
- A将数据包发送到默认网关(通常是路由器的IP地址,如192.168.1.1),而为了实现这一步,需要在局域网范围内先利用ARP请求获取默认网关的MAC地址(如如00:1A:2B:3C:4D:7F)
- 获得默认网关MAC地址后,A会开始构建数据帧,包括:
- 目标MAC地址:00:1A:2B:3C:4D:7F(默认网关的MAC地址)
- 源MAC地址:00:1A:2B:3C:4D:5E(设备A的MAC地址)
- 目标IP地址:203.0.113.5(设备B的IP地址)
- 源IP地址:192.168.1.2(设备A的IP地址)
- 数据:实际要发送的数据
- A将数据帧发送到默认网关
- 路由器接收到数据帧后,检查目标IP地址203.0.113.5,通过路由表确定下一跳地址,将数据包转发到下一个路由器或直接转发到目标网络
- 同时,在转发出去之前,路由器会构建新的数据帧,包括:
- 目标MAC地址:00:1A:2B:3C:4D:6F(设备B的MAC地址,通过ARP请求获取)
- 源MAC地址:路由器接口的MAC地址
- 目标IP地址:203.0.113.5(设备B的IP地址)
- 源IP地址:192.168.1.2(设备A的IP地址)
- 数据:实际要发送的数据
- 路由器发送构建好的数据帧
- B收到数据帧后,检查目标MAC地址和自己的是否匹配,进行进一步处理

注:不能直接将默认网关理解成路由器,而应该是路由器的一个特定接口
一个路由器可能有多个网关连接不同的网络
TCP协议
TCP协议提供可靠的字节流服务
字节流服务:为了传输的方便,TCP协议会将大段数据切分成一段段报文段
可靠:TPC协议能确保数据送达目的地。
TCP协议的可靠主要是通过三次握手策略实现的。
三次握手:
- 第一次握手:
- 发送端发送SYN(SYN=1,ACK=0)数据包,并选择一个初始序列号
- 发送端进入SYN_SENT状态
- 第二次握手:
- 接收端收到SYN数据包后,向发送端发送SYN/ACK数据包(SYN=1,ACK=1),确认发送端的序列号,同时选择自己的序列号
- 接收端进入SYN_RECEIVED状态
- 第三次握手:
- 发送端收到SYN/ACK数据包后,发送一个ACK数据包(SYN=0,ACK=1),确认接收端的序列号
- 发送端和接收端都进入ESTABLISHED状态,连接建立完成。

看起来,第三次握手似乎有些多余,其实也非常重要:
- 表示确实收到了接收端的SYM/ACK数据包
- 当第二次握手结束后,接收端进入了SYN_RECEIVED状态,如果没有第三次握手,接收端无法调整自己的状态,让两边状态一致
- 防止旧的连接请求引起混乱
- 在网络中,数据包可能因为延迟等原因而重复到达
- 发送端的ACK数据包包含了对于接收端序列号的确认,接收端借此判断这是一个正确的ACK数据包而不是旧的请求,避免建立错误连接
- 确保双方都准备好传输数据
- 双向确认:三次握手确保了双方都确认了对方的序列号和窗口大小等参数。发送端通过第三次握手确认了接收端的参数,接收端也通过第二次握手确认了发送端的参数。
- 同步状态:通过三次握手,双方都进入了“已建立连接”的状态,可以开始可靠的数据传输。
- 防止半开连接(Half-Open Connection)
- 半开连接问题:如果只有两次握手,接收端发送SYN/ACK后可能会认为连接已经建立,但发送端可能并没有收到SYN/ACK(例如由于网络问题)。这种情况下,接收端会等待数据,而发送端并不知道连接已经建立,从而导致资源浪费。
- 第三次握手的作用:发送端的ACK数据包确保了双方都确认了连接的存在,避免了半开连接的问题。
除了三次握手意外,TCP还有很多其他机制确保信息能够准确送达
DNS协议
DNS协议主要作用于应用层,它主要负责将域名解析为对应的IP地址或者反过来将IP地址解析为域名
1.5 URL与URI
URI :统一资源标识符
URL:统一资源定位符
这二者都是用来对于资源进行唯一标识,但存在一些差别。
URI
URI是一种通用概念,用于唯一标识资源,不一定包含访问方式(协议)或具体的资源位置
例子:
https://example.com/index.html(一个 URL,也是 URI)mailto:someone@example.com(邮件地址 URI)urn:isbn:0451450523(一个书籍的唯一标识符)
URL
URL 是 URI 的一个子集,它不仅标识资源,还指定了如何访问该资源(包含协议、域名、端口、路径等)。
例子:
https://www.example.com/index.htmlftp://ftp.example.com/file.txtfile:///C:/Users/Admin/doc.txt
所有URL都是URI,但并非所有的URI都是URL
区别:
| 对比项 | URI | URL |
|---|---|---|
| 定义 | 统一资源标识符 | 统一资源定位符 |
| 作用 | 唯一标识资源 | 标识资源+访问方式 |
| 是否包含访问方式 | 不一定 | 必须包含 |
| 示例 | urn:isbn:01234 | https://baidu.com |
| 关系 | URL是URI的子集 | URL是URI的一个特殊类型 |
简单理解:
URL = URI+访问方式
在web开发中,我们通常使用URL,但在更广泛的计算及领域,可能URI更常用
URL格式
协议名: 登录信息@(可选) 服务器地址 端口号(可选)文件路径 查询字符串(可选)资源片段(可选)
第2章 简单的HTTP协议
2.1 HTTP协议基本概念
客户端与服务器端:
- HTTP协议通信基于CS架构,在单次的HTTP通信中,一定会明确的区分出客户端与服务端
- 请求访问文本、图像等资源的一端称为客户端,提供服务的称为服务端或服务器端
- 虽然实际情况中,可能会有双向的请求与服务,但针对每一次HTTP通信,一定是由客户端发起请求,服务端进行响应。
请求与响应:
- HTTP协议中进行信息交互的载体
- 客户端发出的称为请求
- 服务端回复的称为响应
简单的请求例子:
GET /index.htm HTTP/1.1
Host:example.com
请求是按照一行一行进行划分的
第一行称为请求行,是必须的。
- 请求行第一个字段GET称为访问的方法,表明这一次请求属于哪种类型,一般是全部大写的英文单词
- 之后的路径称为请求URL,用来标识这次请求的资源的具体位置
- 最后的HTTP/1.1字段说明了这次请求遵顼的HTTP版本号
请求行之后是一些可选的请求首部字段,如Host,Content-type等,首部字段都以字段名:值的格式出现
请求行之后空一行作为分隔
之后的内容称为请求主体,一般是客户端需要提交给服务端的一些资源,包括JSON数据、表单、图像等资源
简单的响应例子:
HTTP/1.1 200 OK
Content-Type: text/html
<html>
...
响应的格式与请求比较类似,也是按照一行一行进行划分
第一行称为状态行,是必须的
- 第一个字段HTTP/1.1 指定了响应的HTTP协议版本号
- 200是状态码,代表了这次响应的状态,成功、失败等
- OK紧跟状态码,称为原因短语,是对状态码的简单解释
状态行之后类似请求,也是一些可选的响应首部字段
空一行作为分隔
之后的内容称为响应主体,是响应真正要交给客户端的资源
2.2 请求的方法
请求中的方法,可以理解为用来告知服务器这次请求的意图,HTTP/1.1中,有几个比较常见的请求方法
GET
GET方法用来像服务器请求获得请求URL指定的资源,指定的资源经过服务器解析后返回给客户端。
- 如果是文档、图像等静态资源,服务器会保持原样的返回给客户端
- 如果是CGI等程序,服务器会将执行的输出结果返回给客户端
HTTP协议本身没有对响应的大小做出限制,理论上可以请求非常大的资源,但是实际可能收到网络限制、服务器限制等影响
例:
客户端请求一张图片
GET /images/example.jpg HTTP/1.1
Host: www.example.com
服务器的响应:
HTTP/1.1 200 OK
Content-Type: image/jpeg
Content-Length: 12345
[图像数据]
PUT与POST
在HTTP协议中,POST 和 PUT 方法都用于向服务器发送数据,但它们的语义和使用场景有显著区别。
1. 语义区别
POST
- 用途:用于向服务器提交数据,通常用于创建新资源或触发某些操作。
- 幂等性:非幂等。多次相同的POST请求可能会导致不同的结果(例如创建多个资源)。
- 资源位置:由服务器决定新资源的URL。客户端不需要知道资源的具体位置。
PUT
- 用途:用于向服务器上传数据,通常用于更新或替换现有资源。
- 幂等性:幂等。多次相同的PUT请求对资源的影响是相同的(例如多次更新同一资源)。
- 资源位置:客户端需要知道资源的URL,并在请求中指定。
2. 使用场景
POST
- 创建资源:例如提交表单数据创建一个新的用户。
- 触发操作:例如提交一个搜索请求或执行某个操作。
- 不确定资源URL:当客户端不知道新资源的URL时,使用POST。
PUT
- 更新资源:例如更新用户的个人资料。
- 替换资源:例如上传一个文件到指定位置。
- 已知资源URL:当客户端知道资源的URL时,使用PUT。
3. 请求示例
POST 示例
-
请求:
POST /users HTTP/1.1 Host: example.com Content-Type: application/json { "name": "John Doe", "email": "john@example.com" } -
响应:
HTTP/1.1 201 Created Location: /users/123服务器创建了一个新用户,并返回新资源的URL(
/users/123)。
PUT 示例
-
请求:
PUT /users/123 HTTP/1.1 Host: example.com Content-Type: application/json { "name": "John Doe", "email": "john.doe@example.com" } -
响应:
HTTP/1.1 200 OK服务器更新了ID为123的用户信息。
4. 幂等性
- POST:非幂等。多次相同的POST请求可能会导致创建多个资源。
- PUT:幂等。多次相同的PUT请求对资源的影响是相同的。
示例
- POST:
- 第一次请求:创建用户A。
- 第二次请求:创建用户B。
- 结果是两个不同的资源。
- PUT:
- 第一次请求:更新用户A的邮箱。
- 第二次请求:再次更新用户A的邮箱。
- 结果仍然是用户A的邮箱被更新,资源没有变化。
5. 安全性
- POST:通常用于提交数据,可能会修改服务器状态,因此不安全。
- PUT:用于更新或替换资源,可能会修改服务器状态,因此不安全。
6. 缓存
- POST:默认情况下,POST请求的响应不会被缓存。
- PUT:默认情况下,PUT请求的响应不会被缓存。
7.总结
| 特性 | POST | PUT |
|---|---|---|
| 用途 | 创建资源或触发操作 | 更新或替换资源 |
| 幂等性 | 非幂等 | 幂等 |
| 资源位置 | 由服务器决定 | 由客户端指定 |
| 安全性 | 不安全 | 不安全 |
| 缓存 | 默认不缓存 | 默认不缓存 |
- POST 更适合创建资源或触发操作,尤其是当客户端不知道资源URL时。
- PUT 更适合更新或替换资源,尤其是当客户端知道资源URL时。
HEAD
HEAD方法的具体原理与GET十分类似,但是服务器不会对HEAD请求返回具体的资源
HEAD方法一般用来检测URL的有效性和资源更新日期等信息
OPTIONS
OPTIONS用来查询对于指定资源,服务器支持的HTTP请求方法
比如:
OPTIONS * HTTP/1.1
Host:example.com
这里*表示URL不是特定资源,而是整个服务器
服务器响应:
HTTP/1.1 200 OK
ALLOW:GET,POST,HEAD,OPTIONS
2.3 HTTP ——一种无状态协议
2.3.1 无状态协议简介
HTTP协议被称为是一种无状态协议。
状态:在HTTP协议中,状态指的是客户端与服务器之间的交互信息。例如:
- 用户身份:用户是否已经登陆
- 会话数据:用户的购物车内容、浏览历史等
- 请求上下文:客户端之前的请求对当前请求的影响
HTTP协议之所以这样设计,主要是为了简化服务器的实现和提高可扩展性。
简化服务器设计:
- 服务器不需要保存客户的状态信息,每次请求都是独立的,服务器只会处理当前的请求
- 这样可以减少服务器的资源消耗
提高可扩展性:
- 由于每次请求都是独立的,不需要考虑状态同步等问题,服务器可以非常方便的水平扩展(增加服务器数量),每个请求可以被任意服务器处理,并不依赖特定的状态信息
无状态的局限性:
虽然无状态的设计有诸多优点,但也带来了一些问题:
-
无法保持会话:
- 例如,用户登录后,服务器无法记住用户的身份,导致每次请求都需要重新验证身份。
-
无法维护上下文:
- 例如,用户在购物网站中添加商品到购物车后,服务器无法记住购物车的内容。
为了解决无状态协议的局限性同时保留无状态协议的优点,HTTP/1.1引入了Cookie技术。
2.3.2 Cookie技术
服务器可以通过Set-Cookie头部将状态信息发送给客户端,客户端在后续请求中通过Cookie头部将状态信息发送回服务器。
例如:用户正在访问一个在线商店网站,网站上有一个购物车功能。用户选择了一些商品,但没有立即结账。当用户下次访问网站时,网站仍然记得用户之前选择的商品,即便用户并没有登录。这是因为网站使用了Cookie技术来保存用户的一些状态信息。
客户端发送请求(没有购物车信息)
首次访问网站时,浏览器会发送一个 HTTP 请求:
GET /shop HTTP/1.1
Host: www.onlineshop.com
此时,服务器没有收到任何购物车信息,因为你还没有选择商品或没有登录。服务器会生成一个新的购物车状态(通常是一个购物车ID),并在响应中通过 Set-Cookie 头部将这个ID发送给浏览器:
HTTP/1.1 200 OK
Set-Cookie: cart_id=123456789; Path=/; Max-Age=3600
Content-Type: text/html
在这个响应中,Set-Cookie 设置了一个名为 cart_id 的cookie,其值是 123456789,表示你的购物车ID。Max-Age=3600 表示这个cookie将在1小时后过期,Path=/ 指定了这个cookie在整个网站范围内都有效。
用户添加商品到购物车
接下来,假设用户在这个页面上选择了一个商品,并将其添加到购物车中。此时,浏览器会将之前服务器设置的 cart_id cookie 自动带上,发送给服务器:
GET /add_to_cart HTTP/1.1
Host: www.onlineshop.com
Cookie: cart_id=123456789
服务器接收到请求时,会读取到 cart_id 为 123456789,然后根据这个ID找到对应的购物车信息,并将新添加的商品加入到购物车中。之后,服务器可能会更新购物车内容(比如商品数量、总价格等),并再次通过 Set-Cookie 将更新后的购物车ID发送回浏览器:
HTTP/1.1 200 OK
Set-Cookie: cart_id=987654321; Path=/; Max-Age=3600
Content-Type: text/html
这样,服务器更新了 cart_id,并在响应中返回给浏览器。
用户下次访问
一小时后,用户关闭了浏览器,重新打开浏览器并访问网站时,浏览器会自动携带之前保存的 cart_id cookie,发送如下请求:
GET /shop HTTP/1.1
Host: www.onlineshop.com
Cookie: cart_id=987654321
服务器收到请求后,读取到 cart_id=987654321,并根据这个ID从数据库或缓存中找到你的购物车信息。这时,服务器就知道你之前添加的商品,并能够继续为你展示购物车的内容,甚至在你登录后将购物车与账户关联起来。
服务器如何利用 Cookie 保存状态
- 标识用户或会话:通过
cart_id这样的唯一标识符,服务器可以识别出用户的购物车状态。 - 持久化状态信息:即便用户关闭了浏览器或重新访问,服务器依然可以通过Cookie获取到之前的状态,从而为用户提供一致的体验
第3章 HTTP报文的信息
3.1 HTTP报文结构
用于HTTP协议交互的信息称为HTTP报文,客户端发起的报文称为请求报文,服务器端发起的报文称为响应报文
HTTP报文本身是由多行数据构成的字符串文本(HTTP报文中换行符为CR+LF,也就是所谓的回车换行符)
HTTP报文大致可以分为报文首部和报文主体(也称报文主体)两部分,两者用文本中第一次出现的空白行作为分隔;
报文主体并不是必须的

请求报文和响应首部由以下结构组成:
- 请求行:包含请求方法 请求URL HTTP版本
- 状态行(响应首部):包含HTTP版本、状态码和原因短语
- 首部字段:表示请求和响应的各种条件与属性,一般由四种首部:
- 通用首部
- 请求首部
- 响应首部
- 实体首部
- 其他:可能包含Cookie等信息
3.2 HTTP报文的编码
3.2.1编码中的基本概念
字节流 Octet Sequence:
- 字节流(Octet Sequence) 是指由一系列 字节(byte) 组成的数据流。
- 在计算机科学中,一个字节通常由 8位(bit) 组成,每个位可以是 0 或 1。
- 因此,“字节流” 或 “Octet Sequence” 实际上就是一串 8位的二进制数 的集合,每个字节对应一个 Octet。
HTTP通信中,以字节流作为基本单位
实体主体与报文主体:
在HTTP协议中,报文主体(Message Body) 和 实体主体(Entity Body) 是两个容易混淆但有区别的概念。
报文主体(Message Body)
报文主体 指的是 HTTP 请求或响应报文中实际承载数据的部分,它出现在 头部(Header)之后,用于传输数据。
- 在 HTTP 请求中,如果是
POST或PUT请求,报文主体通常包含表单数据、JSON 数据或文件数据。 - 在 HTTP 响应中,报文主体通常包含 HTML、JSON、图片等返回内容。
示例(HTTP 请求):
POST /api/user HTTP/1.1
Host: example.com
Content-Type: application/json
Content-Length: 27
{"username": "Alice", "age": 25}
这里:
- 报文头部(Header) 以
Host、Content-Type、Content-Length等字段组成。 - 报文主体(Message Body) 是
{"username": "Alice", "age": 25}。
实体主体(Entity Body)
实体主体 是 HTTP 报文中的数据内容在应用层的表示,它是 HTTP 报文真正要传输的有效负载(Payload)。
在 HTTP/1.1 及以前,HTTP 采用 实体(Entity) 的概念,一个 HTTP 传输的单位可以分为:
- 实体头部(Entity Header)
- 实体主体(Entity Body)
实体主体基本上等同于报文主体,但当启用了编码或分块传输时,二者可能不同。
关键区别
- 报文主体:实际发送的 数据内容,可能包含编码后的数据或分块传输的数据。
- 实体主体:应用层理解的数据内容,解码后可能与报文主体不同。
具体区别(什么时候不同?)
情况 1:正常传输(两者相同)
如果服务器 没有进行内容编码或分块传输,那么:
实体主体 == 报文主体
例如:
HTTP/1.1 200 OK
Content-Length: 13
Hello, world!
在这里:
- 报文主体 是
Hello, world! - 实体主体 也是
Hello, world! - 它们相同。
情况 2:压缩(两者不同)
如果服务器使用 Content-Encoding 进行 压缩(如 gzip、br、deflate),那么:
实体主体 ≠ 报文主体
例如:
HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Length: 25
(压缩后的二进制数据...)
- 报文主体 是
gzip压缩后的二进制数据。 - 实体主体 是解压缩后的原始 HTML/JSON 等内容。
情况 3:分块传输(两者不同)
当使用 Transfer-Encoding: chunked 进行 分块传输 时,报文主体是 分块编码格式,但实体主体是 拼接后的内容。
示例:
HTTP/1.1 200 OK
Transfer-Encoding: chunked
5
Hello
6
world
0
-
报文主体是 分块编码格式,数据是:
5\r\nHello\r\n6\r\n world\r\n0\r\n -
实体主体是 去掉分块标识后拼接的内容:
Hello world
总结
| 概念 | 作用 | 是否受编码影响 | 示例 |
|---|---|---|---|
| 报文主体(Message Body) | 传输的原始数据 | 是(可以是压缩、分块等格式) | gzip 压缩后或分块数据 |
| 实体主体(Entity Body) | 应用层实际使用的数据 | 否(解码后才是原始数据) | 解压缩或去分块后的 HTML、JSON |
简而言之:
- 在 普通传输 情况下,报文主体 = 实体主体。
- 在 压缩或分块传输 情况下,报文主体 ≠ 实体主体,因为报文主体是 编码后的数据,实体主体是 解码后的内容。
3.2.2 压缩与分块
当我们试图在请求或响应中传输存储空间比较大的实体主体时,为了提高效率,可以采用对字节流进行压缩编码的方式来减少存储空间加快效率
注意:对实体主体的压缩和分块不止服务端响应中可以实现,客户端的请求同样能对实体主体编码与分块
压缩时需要在首部字段中的Content-Encoding属性明确指出编码格式。
HTTP报文中常见的编码格式:
- gizp(GNU zip)
- compress(UNIX系统的标准压缩)
- deflate(zlib)
- identity(不进行编码)
分块传输
分块传输本质就是把实体主体分成一部分一部分的传输,每一部分称为块chunk
分块传输需要指定Transfer-Encoding属性,将其值指定为chunked
每一块开头用16进制数来标记块的大小,最后一块末尾添加0和CRLF换行
例子:
HTTP/1.1 200 OK
Transfer-Encoding: chunked
5
Hello
6
world
0
利用分块传输机制,不仅能加快传输大文件的效率,还能实现断点恢复
所谓的断点恢复就是只客户端每次只从服务器获取资源的一部分,如果网络波动等原因造成下载资源时中断,网络恢复后客户端能从中断的位置继续下载资源
3.3 HTTP协议中的多对象集合
多对象集合:在一次请求或响应中包含多个资源
HTTP协议本身在设计之初,是针对单个资源的,但是随着网络规模发展,为了提高网络传输效率,使用了多种方式让一次HTTP请求或响应中处理多个对象,这种模式称为多对象集合
3.3.1多部份请求或响应
利用HTTP/1.1新增的multipart/*属性,可以在一次传输中传输多个对象,主要适用于 :
- 文件上传(典型的是表单文件的上传)
- 批量请求多个数据对象
- 服务器返回多个资源
使用例子:
假设希望通过一次HTTP请求上传用户提交的一整个表单:
POST /upload HTTP/1.1
Host:example.com
Content-type:multipart/form-data;boundary=Wo_shi_Rooike
--wo_shi_Rooike
Content-Disposition: form-data; name="username"
Alice
--wo_shi_Rooike
Content-Disposition:form-data;name="file1";filename="image1.jpg"
(image1的二进制数据)
--wo_shi_Rookie
Content-Disposition: form-data; name="file2"; filename="image2.jpg"
Content-Type: image/jpeg
(image2的二进制数据)
--wo_shi_Rooike--
使用多部分请求时,首先在Content-type头部中给出multipart的具体取值,这里要传输表单文件,所以使用了值form-data;除此之外,常见的取值还有:
multipart/mixed:主要用在响应中表示返回多个文件multipart/byteranges:主要用于请求或响应资源部分下载
紧跟multipart属性后,需要指定boundary字符串,这个字符串是自定义的,用于在报文主体中分隔不同的对象
之后空一行进入报文主体
每个对象都使用“--"+自定义的boundary字符串作为分割
在对象内部,也分为每个对象自己的实体首部和实体主体,在首部中使用Content-Disposition、Content-Type等属性描述对象,
然后空一格
空一格后是具体的对象数据
最后结束时,在--boundary后再加入--字符,表示结束
响应也是类似的格式
HTTP/1.1 200 OK
Content-Type: multipart/mixed; boundary=abc123
--abc123
Content-Type: text/plain
Hello, this is text.
--abc123
Content-Type: image/jpeg
(binary image data)
--abc123--
3.3.2 HTTP/2中的多路复用
在HTTP/1.1版本,对于每个对象,我们都需要建立一次TCP连接。
HTTP/2 中实现了多路复用技术:
- 可以让一个TCP连接传输多个并行请求,减少连接开销
- 每个请求使用流ID作为区分,服务器可以乱序返回,加快页面加载速度
例子:
客户端请求:
GET /index.html HTTP/2
GET /style.css HTTP/2
GET /script.js HTTP/2
服务器响应:
HTTP/2 200
(stream 1) Content-Type: text/html
<body>...</body>
HTTP/2 200
(stream 3) Content-Type: application/javascript
console.log('Hello');
HTTP/2 200
(stream 2) Content-Type: text/css
body { background: black; }
这里服务器并不需要按照顺序返回,因此不必等一个请求处理完成后再处理下一个。
注:截止目前,对于HTTP/2,大部分主流浏览器和服务器都可以支持
3.3.3合并JSON响应
在REST API设计中,客户端可能需要多个资源,为了减少HTTP请求次数,服务器页可以依从性返回多个对象
3.3.4 HTTP Range 请求
如果客户端只需要请求一部分资源,可以使用Range头部,在响应中添加multipart/byteranges字段
例子:
请求(在Range中指出要下载的具体范围):
GET /video.mp4 HTTP/1.1
Range: bytes=0-1023, 2048-3071
服务器返回:
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=xyz
--xyz
Content-Range: bytes 0-1023/5000000
(binary data)
--xyz
Content-Range: bytes 2048-3071/5000000
(binary data)
--xyz--
3.3.5 WebSocket/SSE(Server-Sent Events)
传统的HTTP是请求-响应模式,如果用户需要多个动态数据对象,可以使用:
- WebSocket :全双工通信,适用于聊天、游戏
- SSE :服务器推送数据,适用于实时通知
例子:
请求:
GET /events HTTP/1.1
Accept: text/event-stream
服务器持续推送多个数据对象:
data: { "message": "Hello" }
data: { "message": "New Notification" }
总结
| 方法 | 使用场景 | 优点 |
|---|---|---|
multipart/* | 上传多个文件、API批量数据返回 | 一次传输多个请求对象 |
| HTTP/2多路复用 | 并行加载多个网页资源 | 提高加载速度,减少连接数 |
| JSON批量请求API | 需要获取多个关联数据 | 降低请求次数,减少HTTP头部开销 |
| Range请求 | 断点续传、部分下载 | 只下载需要的数据,提高效率 |
| WebSocket/SSE | 实时通信、多对象推送 | 持久连接,减少延迟 |
5.SQL语句学习 _SELECT语句
第二课 检索数据
2.1 SQL语句基本介绍
SQL 结构化查询语言:
- 是一种用于管理和操作关系型数据库的标准编程语言
关系型数据库:
- 以表table的形式存储数据。表由行(记录)和列(字段)组成
- 关系型数据库的核心是表之间的关系,SQL通过
JOIN操作将多个表数据关联起来,来实现设计逻辑
SQL的主要功能:
-
数据查询:SQL的核心功能。使用
SELECT语句从表中提取需要的数据,可以使用条件、排序、分组等操作过滤和组织结果 -
数据操作:SQL支持对数据进行一系列操作,主要包括:
-
插入数据:向表中添加新记录,使用
INSERT INTO语句 -
更新数据:修改表中现有的记录,使用
UPDATE语句 -
删除数据:从表中删除记录,使用
DELETE语句
-
-
数据定义:SQL允许用户定义和修改数据库结构:
- 创建表:定义新表或添加新字段,使用
CREAT TABLE语句 - 修改表: 添加、删除或修改表的字段,使用
ALTRE TABLE语句 - 删除表:使用
DROP TABLE语句
- 创建表:定义新表或添加新字段,使用
-
数据控制:
- SQL支持对数据库的访问控制和权限管理
- 使用
GRANT和REVOKE语句授予或撤销用户对数据库对象的访问权限
-
聚合与分组:
- SQL支持对数据进行聚合计算(如求和、平均值、计数等)
- SQL也支持分组操作(
GROUP BY)
-
事务管理:
- SQL支持事务(Transaction),即一组要么全部成功要么全部失败的操作
- 通过
BEGIN、COMIT和ROLLBACK语句,用来确保数据的一致性和完整性
2.2 SELECT语句基本使用
SQL的语句都由关键字和数据组成
SELECT语句是最常见的SQL语句
SELECT 列名
FROM 表名;
SELECT FROM 称为关键字,SQL中没有严格规定关键字的大小写,但是一般习惯SQL关键字全部大写,表名和字段名全部小写
SQL语句不会检查换行符,习惯上一个子句占一行
SQL语句以分号结尾
SQL语句不像一般的程序设计程序,一般一条语句就都能得到执行结果
SELECT语句用于从表中查询一列的数据:
SELECT cust_name
FROM customers;

可以利用一条SELECT语句查询多个列,列名之间用逗号隔开,最后一个列名不能加逗号。
会按照语句中输入的顺序返回列
SELECT cust_id,cust_name,cust_city
FROM customers;

也可以使用通配符检索表中的所有列
SELECT *
FROM customers;

利用DISTINCT关键字,可以限制SELECT语句的输出结果,只输出不同的值
SELECT DISTINCT cust_name
FROM customers;

如果希望限制输出结果,不全部输出,在MySQL中,可以使用LIMIT子句
SELECT prod_name
FROM products
LIMIT 5 OFFSET 5;
OFFSET指示了从哪一行开始检索的行索引,LIMIT指示了检索多少行。
行数索引从0开始

不同的DBMS中,对于限制这一子句有不同的实现
如Oralce中,需要基于ROWNUM(行计数器)来计算行
SELECT prod_name
FROM products
WHERE ROWNUM <=5;
第三课 排序检索数据
排序数据
使用SELECT语句的ORDER BY 子句,可以实现对检索数据的排序。
- 如果直接使用SELECT检索到的数据,数据会按照在数据库表中的顺序进行输出;
- 这个顺序一开始可能是录入数据时的顺序,但如果之后有删除或更新,这个顺序会受到DBMS重用回收存储空间的方式影响。
- 因此,关系数据库设计理论认为,如果不明确规定排序顺序,则不应该假定检索出的数据的顺序有任何意义
格式:
- SELECT指定要检索的列,
- ORDER BY子句指定根据哪一列的顺序对要输出的列排序;这里可以指定与输出列不同的列。
- ORDER BY 子句必须是SELECT语句的最后一个子句
SELECT prod_name
FROM Products
ORDER BY prod_name;

排序结果按照字典顺序升序排列
注意:
-
输出的前三条记录的pro_name字段,
12 inch...在第一个 ,之后是18 inch...,最后是8 inch... -
并没有按照前面数字的大小进行排序
-
因为MySQL中默认所有数据都是字符串,这里12 18 8 三个数据,1的字符顺序比8靠前,因此这两条排到了更前面
-
如果希望按照数字大小顺序排序,可以考虑提取数字内容单独作为一列,
-
或者使用一些更复杂的关键字来满足需求
-
以下是GPT4-o给我提供的参考:
-
SELECT prod_name FROM products ORDER BY CASE WHEN prod_name REGEXP '^[0-9]+' THEN 1 -- 以数字开头的设为 1 ELSE 2 -- 以字母开头的设为 2 END, CAST(prod_name AS UNSIGNED), -- 数字排序(对以数字开头的数据) prod_name; -- 字母按字典顺序排序

可见,SQL语句的语法并不复杂,但是合理设计的SQL语句可以实现非常强大的功能
按多个列排序
可以按照不止一个列的顺序排序。
当ORDER BY后面出现了多个列后,会优先按照第一个列的顺序排序,之后,如果有记录的第一个字段值相同,再按照第二列的顺序排序,依次类推。
SELECT prod_id,prod_price,prod_name
FROM products
ORDER BY prod_price,prod_name;

指定排序方向
ORDER BY子句默认按照升序顺序排序,如果希望按照降序排序,可以指定DESC关键字表示降序
SELECT prod_id,prod_price,prod_name
FROM products
ORDER BY prod_price DESC;

注意如果按照多个列的顺序进行排序时,DESC只做用于离它最近的列,不会影响其他列
实际上还存在一个与DESC对应的关键字ASC用来指定按照升序顺序排列,但这个关键字意义不大,因为ORDER BY默认就会按照升序排列
后记
1.本来这周我还计划学习liunx操作系统,买了网友们推荐的《Liunx私房菜》。但我本身属于计算机基础差的,读这本书读到100多页了还没有开始讲Liunx实在优点看不懂了,目前在寻找更合适我的Liunx学习书籍,若有大神愿意指点我也非常感激!
2. 上周总结笔记的时候由于我对git的误操作,丢失了一整周的学习记录,当时心态差点崩溃,最终还是缓过来重新对上周学习的指示重新总结。
3. 以后尽量把博客写的更加精简一些。
本周阅读书籍:
- 《Java核心技术卷Ⅰ》
- 《图解HTTP协议》
- 《MySQL必知必会》
- 《正则表达式必知必会》
- 《大话数据结构》

5998

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



