一、java的特性是什么?
二、什么是封装、继承、多态(面向对象的角度,最好用项目中 / 生活中的例子),贯穿始终,看你能不能讲清楚)?再讲一下什么是抽象?(我举了汽车的例子)
三、面向对象 和 面向过程有什么区别?
四、for循环有多少中写法?(常见的写法,不包含迭代器)
五、for循环分成好几部分,讲它的每一部分和每一部分的作用。
六、数组,有五个元素【1,2,3,4,5】,如何通过for循环打印1 3 5 用for循环怎么写?
while循环打印 数组【1,2,3,4,5】怎么写?
七、多线程了解吗?举一个多线程的例子?什么情况下用多线程做事情?用多线程要注意什么问题?
八、比如说有两个线程、一个文件,一个线程要更新文件中的内容,另一个线程要读取里面的内容,这个时候代码的执行的循序是什么?,包括加锁、写文件、解锁这些顺序是什么?
九、线程之间,两个线程在并行做事情,那两个线程有时候也要进行一些通信,如何进行通信呢?
十、private、protect、public 三种修饰符有什么区别?
十一、栈和队列有什么区别?举个生活中的例子?(讲清楚,讲完全)
十二、二叉树什么是平衡二叉树?堆排序中堆是一种什么二叉树?
十三、你知道的排序算法有多少种?(说至少五种),选一种讲清楚?
十四、MySQL中有一张表,
A:学号 姓名
B:学号 性别
①连表查出来学号,姓名,性别 ?
②如果要清空一个表的数据用什么命令?
③更新某个学生的姓名,怎么更新?
④删一个表命令
十五、redis是一个什么样的数据库?和别的数据库比它的显著特征?
十六、redis一般作为缓存系统,因为读写速度比较快,那它的缺陷是什么?
十七、计算机网络分层结构,七层,从下往上是什么?
十八、linux关于目录和文件的基本命令?(说十个)
查文件中的某个词用什么命令?
只想查看前十行?
后十行?
十九、①查看一个机器的 ip 地址,想连接一个机器,登录一个机器用什么命令?
②看当前到别的服务器网络通不通用什么命令?
③看他的端口号能不能连接上这个用什么命令?
二十、域名解析具体是做什么事情?可以举个例子吗?
二十一、前后端协作开发,后端写的接口文档内容有什么?
二十二、讲一下项目经历?每段控制在3分钟,两个都介绍一下
(项目背景、为什么做这个项目、有什么功能、项目的架构、是怎么设计这个项目的、这个项目过程中间,分模块设计,设计过程中有没有遇到什么问题,开发完了后是要测试,你具体怎么测的、有没有分场景、写用例,最后这个项目的背景亮点在哪,用到的核心技术)
二十三、事务的话和普通的SQL有什么区别?
二十四、k8s 和 docker容器听说过吗? docker部署几步就能把mysql数据库部署起来?
二十五、docker中容器运行期间,你有没有看过容器的日志?
二十六、docker的网络你知道吗,网桥的名字叫什么,docker0你知道吗
有没有听过容器的主机网络模式?
二十七、有没有了解过什么是云,腾讯云嘛,云包含几层?有没有听过 IaaS、PaaS、SaaS?
CI/CD?
一、java的特性是什么?
封装、继承、多态
二、具体什么是封装、继承、多态(面向对象的角度,最好用项目中 / 生活中的例子),贯穿始终,看你能不能讲清楚)?再讲一下什么是抽象?(我举了汽车的例子)
那我拿汽车举一个例子,
封装:的话就是将发动机、变速箱这些隐藏在内部,对外只保留接口,比如说油门呀,刹车呀。
作用:这么做不仅能保护发动机和变速箱,还能方便用户操作,不需要理解发动机原理就能踩油门加速。
继承是什么呢?比如汽车这个大类,那么新能源电动汽车、燃油车这些就可以继承汽车这个大类,比如说继承它的轮子、外壳、方向盘。比如说电动车还可以实现自己的电池、充电这些特性。
作用:实现了共性的抽取和复用。
多态呢?不同对象调用同一个方法,表现出不同的行为。汽车实例化的奔驰、宝马、特斯拉,按喇叭时都发出 “哔~” 声,但具体音色可能不同。
接口 / 抽象类:定义统一方法(如honk()
)。动态绑定:运行时根据实际对象类型决定调用哪个实现。可扩展性:新增车型(如BMW
)时,无需修改honkAll()
方法。
- 通过父类引用指向子类对象实现多态。
抽象:就是提取共性,用抽象类或接口定义规范。比如这里汽车就可以定义为一个抽象类。它可以规定 “必须有方向盘、四个轮子。但不规定具体用什么材料做的。
关键点:
抽象类 / 接口:不能实例化,仅作为规范(如Car
不能直接new Car()
)。强制实现:抽象方法(如start()
)必须由子类实现。分离设计与实现:上层代码只依赖抽象(如Car
接口),不依赖具体类。
官方定义:
接口是一种抽象类型,它只定义方法签名(名称、参数、返回值),但不包含方法体。实现接口的类必须实现接口中所有的方法。接口呢?如电动车中的充电接口、只要符合这个接口标准,任何充电桩都能充电。
关键作用:
汽车场景中的作用:
标准化:所有电动车和充电桩必须遵循同一接口标准(如国标),否则无法兼容。解耦:车厂不用关心充电桩的实现,充电桩厂商也不用关心车的品牌。扩展性:未来可以新增接口标准(如无线充电接口),而不影响现有系统。
Java中接口的作用:
标准化:规定类必须实现的方法,确保不同类遵循统一标准。实现多继承:一个类可同时实现多个接口,弥补单继承的局限。解耦代码:通过面向接口编程,降低模块间依赖,提高扩展性。支持回调:接口可作为参数传递,实现代码的灵活调用(如事件监听)。
那我举一个汽车的例子
1. 封装(Encapsulation)
定义:将数据(属性)和操作数据的方法(行为)捆绑在一起,并隐藏内部实现细节,仅对外提供必要接口。
类比:汽车的发动机舱。通俗解释:
封装的话就是比如把发动机、变速箱藏在引擎盖下面,只留给用户油门、刹车等来控制速度。
关键作用:
- 保护数据:用户不能直接修改发动机转速,只能通过油门间接控制。
- 简化操作:用户不需要懂发动机原理,踩油门就能加速。
代码示例:
public class Car { private int speed; // 隐藏速度,不让外部直接修改 // 提供公共方法控制速度 public void pressAccelerator(int force) { if (force > 0) { speed += force; if (speed > 200) speed = 200; // 限速保护 } } public int getSpeed() { return speed; } }
2. 继承(Inheritance)
定义:子类(派生类)继承父类(基类)的属性和方法,实现代码复用和扩展。
类比:汽车的分类体系。
- 父类:
Car
(有speed
、start()
等通用属性和方法)。- 子类:
ElectricCar
和GasCar
继承Car
,并扩展各自特性。通俗解释:
所有汽车(子类)都继承 “汽车” 这个大类的基本特性(轮子、方向盘),但可以添加自己的特色(如给汽车的充电功能)。// 父类:普通汽车 public class Car { public void drive() { System.out.println("开车前进"); } } // 子类:电动车 public class ElectricCar extends Car { public void charge() { // 新增充电功能 System.out.println("新增充电功能"); } @Override public void drive() { // 修改父类行为 System.out.println("电动引擎安静地启动"); } }
3. 多态(Polymorphism)
定义:同一方法调用,根据对象类型不同表现出不同行为。
类比:汽车的喇叭。
- 无论是什么品牌的汽车(奔驰、宝马、特斯拉),按喇叭时都发出 “哔~” 声,但具体音色可能不同。
关键点:
- 接口 / 抽象类:定义统一方法(如
honk()
)。- 动态绑定:运行时根据实际对象类型决定调用哪个实现。
- 可扩展性:新增车型(如
BMW
)时,无需修改honkAll()
方法。代码示例:
// 接口:定义汽车的行为 public interface Car { void honk(); // 按喇叭方法 } // 奔驰车 public class Benz implements Car { @Override public void honk() { System.out.println("优雅的:哔~"); } } // 特斯拉 public class Tesla implements Car { @Override public void honk() { System.out.println("电子合成音:哔~"); } } // 测试多态 public class Main { public static void main(String[] args) { Car benz = new Benz(); Car tesla = new Tesla(); honkAll(benz, tesla); // 统一调用按喇叭方法 } public static void honkAll(Car... cars) { for (Car car : cars) { car.honk(); // 不同汽车的喇叭声自动区分 } } }
4. 抽象(Abstraction)
定义:忽略细节,提取共性,用抽象类定义规范。
类比:汽车设计蓝图。通俗解释:
汽车设计图只规定 “必须有四个轮子”“必须能启动”,但不规定具体用什么材料(钢轮还是铝轮)。关键点:
- 抽象类 / 接口:不能实例化,仅作为规范(如
Car
不能直接new Car()
)。- 强制实现:抽象方法(如
start()
)必须由子类实现。- 分离设计与实现:上层代码只依赖抽象(如
Car
接口),不依赖具体类。// 抽象类:汽车设计蓝图 public abstract class Car { public abstract void start(); // 抽象方法:必须启动 public void stop() { // 具体方法:默认实现 System.out.println("踩刹车停车"); } } // 燃油车实现 public class GasCar extends Car { @Override public void start() { System.out.println("点火启动燃油发动机"); } } // 电动车实现 public class ElectricCar extends Car { @Override public void start() { System.out.println("按下启动按钮,电动引擎就绪"); } }
代码示例:
// 抽象类:汽车设计蓝图 public abstract class Car { public abstract void start(); // 抽象方法:必须启动 public void stop() { // 具体方法:默认实现 System.out.println("踩刹车停车"); } } // 燃油车实现 public class GasCar extends Car { @Override public void start() { System.out.println("点火启动燃油发动机"); } } // 电动车实现 public class ElectricCar extends Car { @Override public void start() { System.out.println("按下启动按钮,电动引擎就绪"); } }
5. 接口(Interface)
官方定义:
接口是一种抽象类型,它只定义方法签名(名称、参数、返回值),但不包含方法体。实现接口的类必须实现接口中所有的方法。汽车类比:
- 充电接口(如国标、特斯拉超充)就是一种物理接口,它规定:
- 必须有几个针脚(如直流正极、负极、通信线)。
- 每个针脚的功能和电压标准。
- 插头的形状和尺寸。
- 但不规定:
- 充电桩内部如何转换电能(用 IGBT 模块还是 SiC 模块)。
- 具体用什么品牌的充电桩。
通俗解释:
汽车的 “充电接口”(如 Type-C、国标快充口),只要符合这个接口标准,任何充电桩都能充电。关键作用:
汽车场景中的作用:
- 标准化:所有电动车和充电桩必须遵循同一接口标准(如国标),否则无法兼容。
- 解耦:车厂不用关心充电桩的实现,充电桩厂商也不用关心车的品牌。
- 扩展性:未来可以新增接口标准(如无线充电接口),而不影响现有系统。
3. 代码示例:汽车充电接口
// 充电接口(规定必须有充电方法) public interface Chargeable { void charge(); } // 特斯拉实现了充电接口 public class Tesla implements Chargeable { public void charge() { System.out.println("插入Type-C充电桩,开始充电"); } } // 比亚迪也实现了充电接口 public class BYD implements Chargeable { public void charge() { System.out.println("插入国标充电桩,开始充电"); } }
总结对比
三、面向对象 和 面向过程有什么区别?
首先,
面向对象:是一种以对象为中心的编程风格,把类或对象作为基本单元来组织代码。
并且运用提来出来的:封装、继承、多态来作为代码设计指导。
面向过程:是一种以过程或函数为中心的编码风格。以过程作为基本单元来组织代码。
主要区别如下:
1.思维方式:
面向对象:通过定义对象的属性和行为来解决问题,关注对象之间的关系和交互
面向过程:通过函数或过程一步一步实现业务逻辑,关注执行的步骤和顺序。
2.数据与方法的关系:
面向对象:数据和方法封装在对象内部,数据操作由对象方法进行管理。
面向过程:数据和方法是分离的,函数对数据进行操作。
3.可扩展性和复用性:
面向对象:通过继承、接口、多态等机制支持代码的高复用性和扩展性
面向过程:复用性较低,扩展需要修改已有代码,影响整体稳定性。
4.适用场景
面向对象:适合处理复杂的系统和模块化设计,便于维护和发展
面向过程:适用于一些简单的,顺序性强的小型程序,开发效率较高。
四、for循环有多少种写法?(常见的写法,不包含迭代器)
①普通for
循环
这是最基本的
for
循环形式,通过设置循环变量、循环条件以及更新循环变量的方式来控制循环的执行。
// 示例:计算1到10的整数之和
public class ForLoopExample {
public static void main(String[] args) {
int sum = 0;
for (int i = 1; i <= 10; i++) {
sum += i;
}
System.out.println("1到10的整数之和为:" + sum);
}
}
②增强for
循环(for-each
循环)
增强
for
循环主要用于遍历数组或实现了Iterable
接口的集合(如List
、Set
等),它的语法更加简洁,不需要显式地操作索引。
// 示例:遍历数组并打印每个元素
public class ForEachLoopExample {
public static void main(String[] args) {
int[] numbers = {1, 2, 3, 4, 5};
for (int num : numbers) {
System.out.print(num + " ");
}
}
}
下面这段代码展示了使用增强for
循环遍历List
集合的过程。
import java.util.ArrayList;
import java.util.List;
public class ForEachListExample {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("apple");
fruits.add("banana");
fruits.add("cherry");
for (String fruit : fruits) {
System.out.println(fruit);
}
}
}
③ 标签for
循环
标签
for
循环用于在嵌套循环中,更灵活地控制循环的执行和跳出。可以给外层循环添加一个标签,然后使用break
或continue
语句配合标签,实现直接跳出外层循环或者直接进入外层循环的下一次迭代。
// 示例:使用标签for循环跳出嵌套循环
public class LabeledForLoopExample {
public static void main(String[] args) {
outerLoop:
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
if (i == 2 && j == 2) {
break outerLoop; // 直接跳出外层循环
}
System.out.println("i = " + i + ", j = " + j);
}
}
}
}
在上述代码中:
outerLoop:
:这是给外层for
循环定义的标签。break outerLoop;
:当满足条件i == 2 && j == 2
时,直接跳出被标记为outerLoop
的外层循环,而不是仅仅跳出内层循环。在这个例子中,当满足条件
i == 2 && j == 2
时,continue outerLoop;
语句会让程序跳过内层循环剩余的代码,直接进入外层循环的下一次迭代 。
五、for循环分成好几部分,讲它的每一部分和每一部分的作用。
for (初始化表达式; 循环条件; 迭代表达式) {
循环体
}
六、数组,有五个元素【1,2,3,4,5】,如何通过for循环打印1 3 5 用for循环怎么写?
int[] arr = {1, 2, 3, 4, 5};
for (int i = 0; i < arr.length; i += 2) { // i每次递增2
System.out.println(arr[i]);
}
while循环打印 数组【1,2,3,4,5】怎么写?
先定义一个变量 比如说 i = 0
然后 i < 数组 长度
打印数字
i++
进入下一次循环。
int[] arr = {1, 2, 3, 4, 5};
int i = 0; // 初始化索引
while (i < arr.length) {
System.out.println(arr[i]); // 打印当前元素
i++; // 索引递增
}
七、多线程了解吗?举一个多线程的例子?什么情况下用多线程做事情?用多线程要注意什么问题?
①、多线程的例子(用做饭类比)
假设你要做一顿饭,需要完成三件事:
- 煮米饭(需要 10 分钟)
- 炒青菜(需要 5 分钟)
- 煎鸡蛋(需要 3 分钟)
单线程方式:
- 先煮米饭 → 等米饭熟了再炒青菜 → 再煎鸡蛋,总耗时
10+5+3=18分钟
。多线程方式:
- 启动一个线程煮米饭(同时进行),主线程先炒青菜,再煎鸡蛋。
- 总耗时≈10 分钟(米饭煮好的时间,其他事情在等待米饭时完成)。
②、什么情况下用多线程?
简单说:需要 “同时做多个事情” 来提高效率,或避免程序 “卡住”。
常见场景:
- 耗时操作不阻塞主线程:
- 如 App 加载时,后台线程下载图片,主线程继续响应用户点击(否则界面会卡顿)。
- 充分利用 CPU 资源:
- 处理大量数据(如统计多个文件的字数),用多线程同时处理不同文件,比单线程逐个处理快。
- 并发任务处理:
- 服务器同时接收多个用户的请求(如微信同时接收多条消息),每个请求用一个线程处理。
③、用多线程要注意的问题
线程安全问题:
- 多个线程同时操作共享数据(如多个线程同时修改一个变量),可能导致结果错误。
- 例:两个线程同时给账户存钱,原本存 2 次 100 元,结果可能只存了 100 元。
- 解决:用
synchronized
锁或Lock
保证同一时间只有一个线程操作共享资源。死锁:
- 两个线程互相等待对方释放资源,导致都卡住。
- 例:线程 A 持有锁 1,等待锁 2;线程 B 持有锁 2,等待锁 1,永远僵持。
- 解决:按固定顺序获取锁,避免循环等待。
线程过多消耗资源:
- 每个线程占用内存,线程太多会导致内存不足或 CPU 切换频繁(“上下文切换” 耗时)。
- 解决:用线程池(如
ThreadPoolExecutor
)控制线程数量。线程通信复杂:
- 线程间需要协作(如 “生产者 - 消费者” 模型),若处理不好会导致逻辑混乱。
- 解决:用
wait()
/notify()
或BlockingQueue
等工具协调线程。
八、比如说有两个线程、一个文件,一个线程要更新文件中的内容,另一个线程要读取里面的内容,这个时候代码的执行的循序是什么?,包括加锁、写文件、解锁这些顺序是什么?
加解锁的一个线程:
只读锁(共享的):多个读线程可以同时读取文件。
若一个读的线程先获取到资源,那么就要加一个只读锁、
在只读锁没有被释放的时候,别的线程就不能去更新(写)这个文件。
如果A线程把这个文件读完了,这个时候就可以把锁给释放掉。
就是说这个文件我不用了,
写锁(排他的):同一时间只允许一个线程写入,且写入时不允许其他线程读取。
解锁之后更新的线程可以获取一把写锁
他告诉别人我正在写,你们看着读,写完之后他把锁一解。
九、线程之间,两个线程在并行做事情,那两个线程有时候也要进行一些通信,如何进行通信呢?
线程间通信是多线程编程核心概念,在多线程编程中,有时候需要让线程之间进行数据交换,协作工作。以下是几种线程间的通信方式:
1.共享内存:线程之间可以通过访问同一块共享内存区域来实现数据交换
2.消息队列:一个线程向消息队列中放入一条消息,另一个线程从消息队列中取出消息
3.管道(Pipe):管道是一种半双工的通信方式,一个进程可以向管道中写入数据,另一个线程可以从管道中取出数据
4.信号(signal):信号是一种异步通信方式,进程收到信号后,会根据信号的类型做出相应的判断
5.互斥锁(Mutex):用于同步访问共享资源,防止多个线程同时访问共享资源,产生冲突
6.条件变量:用于线程之间的协调和通信,一个线程可以通过条件变量等待某个条件的出现,另一个线程可以通过条件变量通知正在等待的线程。
7.RPC调用:远程过程调用(RPC)是一种跨网络进行远程调用,可以实现在不用的线程或机器之间进行信息交换。
还可以延伸到你在编程时怎么让多线程共享变量,避免冲突。
十、private、protect、public 三种修饰符有什么区别?
十一、栈和队列有什么区别?举个生活中的例子?(讲清楚,讲完全)
- 栈:后进先出(LIFO),
- 队列:先进先出(FIFO),类似排队,先到的人先服务。
栈,可以类似一摞书,最后放上去的最先被拿走。
队列,类似过安检,先到的人先出。
十二、二叉树:
什么是平衡二叉树?堆排序中堆是一种什么二叉树?
平衡二叉树:
每个节点的左右子树高度差不超过 1,且左右子树均为平衡二叉树。
这种结构能保证树的高度始终保持在 O(log n),从而实现高效的插入、删除和查询操作(时间复杂度均为 O(log n))。
关键特点:
- 任何节点的左右子树高度差(平衡因子)只能是 -1、0、1。
- 插入或删除节点后,可能需要通过 旋转操作(左旋、右旋、左右旋、右左旋)来重新平衡树。
示例:
3 // 平衡因子:左子树高度2 - 右子树高度1 = 1(平衡) / \ 1 4 \ 2 // 所有子树的平衡因子均为0或±1
堆排序中的堆(Heap)
定义:
堆是一种 完全二叉树,分为两种类型:
- 大根堆(Max-Heap):每个节点的值都 大于或等于 其子节点的值,根节点为最大值。
- 小根堆(Min-Heap):每个节点的值都 小于或等于 其子节点的值,根节点为最小值。
关键特点:
- 完全二叉树:除最后一层外,每一层都被完全填充,且最后一层的节点都靠左排列。
- 高效的堆操作:
- 插入元素:O (log n)
- 删除堆顶元素:O (log n)
- 构建堆:O (n)
示例(大根堆):
9 // 根节点值最大 / \ 7 8 // 每个节点的值 ≥ 子节点的值 / \ / \ 5 6 3 4
示例(小根堆):
1 // 根节点最小 / \ 3 2 // 父节点1 ≤ 子节点3、2;父节点3 ≤ 子节点5、4 / \ / 5 4 6 // 所有父节点均满足 ≤ 子节点
堆排序原理:
利用堆的特性,每次取出堆顶元素(最大值 / 最小值),并调整剩余元素为新堆,重复此过程得到有序序列。对比总结:
十三、你知道的排序算法有多少种?(说至少五种),选一种讲清楚?
常见的排序算法包括:冒泡排序、选择排序、插入排序、快速排序、归并排序、堆排序、希尔排序、基数排序等。
1.冒泡排序(稳定)(交换)
(Bubble Sort)核心思想:重复遍历数组,比较相邻元素,若顺序错误则交换,每一轮将最大(或最小)元素 “冒泡” 到末尾,直到整个数组有序。
步骤(以升序为例)
- 初始状态:整个数组视为未排序。
- 第 1 轮:从第 1 个元素开始,比较相邻元素,若前一个 > 后一个,则交换。遍历结束后,最大元素 “冒泡” 到末尾。
- 第 2 轮:重复上述过程,但只遍历到倒数第 2 个元素(因最后一个已排好)。
- 重复:每轮减少遍历范围,直到未排序部分只剩 1 个元素。
// 1. 冒泡排序(稳定,O(n²))
public static void bubbleSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
示例(数组:[5, 3, 8, 4, 6])
初始:[5, 3, 8, 4, 6]
第1轮:比较5-3→交换,3-8不变,8-4→交换,8-6→交换 → [3, 5, 4, 6, 8](最大元素8移至末尾)
第2轮:比较3-5不变,5-4→交换,5-6不变 → [3, 4, 5, 6, 8](次大元素6移至倒数第2位)
第3轮:比较3-4不变,4-5不变 → [3, 4, 5, 6, 8]
第4轮:比较3-4不变 → [3, 4, 5, 6, 8](已完成)
最终排序:[3, 4, 5, 6, 8]
关键特点
- 时间复杂度:
- 最坏 / 平均:O (n²)(需 n (n-1)/2 次比较)。
- 最好:O (n)(数组已有序时,通过设置 “交换标志” 提前终止)。
- 空间复杂度:O (1),原地排序。
- 稳定性:稳定(相等元素不交换,相对顺序不变)。
2.快速排序(不稳定)(交换)
核心思想
先选取一个基准元素,再用分区:将所有比这个基准元素小的数放在基准元素左边,大的放在右边。再这样递归对左边右边两部分分别排序,最终使整个数组有序。
步骤(以升序为例)
- 选择基准值:从数组中任选一个元素作为 “基准”(通常选第一个、最后一个或中间元素)。
- 分区操作:将数组中小于基准值的元素放在基准左侧,大于基准值的元素放在右侧(等于基准值的可放左或右)。
- 递归排序:对基准左侧和右侧的子数组重复上述步骤,直到子数组长度为 1(天然有序)。
// 4. 快速排序(不稳定,O(n log n))
public static void quickSort(int[] arr) {
quickSort(arr, 0, arr.length - 1);
}
private static void quickSort(int[] arr, int low, int high) {
if (low < high) {
int pivotIndex = partition(arr, low, high);
quickSort(arr, low, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, high);
}
}
private static int partition(int[] arr, int low, int high) {
int pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; j++) {
if (arr[j] < pivot) {
i++;
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;
return i + 1;
}
示例(数组:[3, 6, 8, 10, 1, 2, 1])
1.选基准值为 3,分区后
左侧为 [1, 2, 1](均 < 3),
右侧为 [6, 8, 10](均 > 3),
数组变为 [1, 2, 1, 3, 6, 8, 10]。
递归处理左侧 [1, 2, 1]:
选基准 1,
分区后左侧为空,右侧为 [2, 1](均 >= 1),
再对 [2, 1] 分区(基准 2,左侧为 [1],右侧为空),
最终左侧子数组排序为 [1, 1, 2]。
递归处理右侧 [6, 8, 10]:
选基准 6,分区后右侧为 [8, 10],
继续递归排序,
最终右侧为 [6, 8, 10]。
合并结果:[1, 1, 2, 3, 6, 8, 10]。
关键特点
- 时间复杂度:平均 O (n log n),最坏 O (n²)(如有序数组选两端为基准),可通过随机选基准优化。
- 空间复杂度:O (log n)(递归栈开销),不稳定排序(相等元素可能交换位置)。
- 优势:实际应用中效率高,是工业界常用的排序算法(如 C++ 的
std::sort
部分场景采用)。
3.选择排序(不稳定)(选择)
核心思想:
1.从未排序序列中找到最小(或最大)元素
2.将其放到已排序序列的末尾
重复上述过程,直到所有元素排序完毕
步骤(以升序为例)
- 初始状态:整个数组视为未排序。
- 第 1 轮:从未排序部分(全体元素)中找到最小值,与第 1 个元素交换。
- 第 2 轮:从未排序部分(第 2 个元素至末尾)中找到最小值,与第 2 个元素交换。
- 重复:以此类推,直到未排序部分只剩最后一个元素(天然有序)。
// 2. 选择排序(不稳定,O(n²))
public static void selectionSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
int temp = arr[minIndex];
arr[minIndex] = arr[i];
arr[i] = temp;
}
}
示例(数组:[64, 25, 12, 22, 11])
初始:[64, 25, 12, 22, 11]
第1轮:最小值11,与64交换 → [11, 25, 12, 22, 64]
第2轮:最小值12,与25交换 → [11, 12, 25, 22, 64]
第3轮:最小值22,与25交换 → [11, 12, 22, 25, 64]
第4轮:最小值25,无需交换 → [11, 12, 22, 25, 64]
最终排序:[11, 12, 22, 25, 64]
关键特点
- 时间复杂度:始终为 O(n²),无论数组初始状态如何。
- 空间复杂度:O (1),原地排序(仅需常数级额外空间)。
- 稳定性:不稳定(例如 [5, 8, 5, 2] 中两个 5 的相对顺序可能改变)。
- 优势:简单直观,适合小规模数据或内存受限场景。
4、堆排序(不稳定)
(Heap Sort)核心思想:利用 “堆” 这种数据结构(完全二叉树)的特性,先将数组构建为大根堆(或小根堆,此处以大根堆为例),再通过反复提取堆顶元素(最大值)并调整堆结构,最终得到有序数组。
步骤(以升序为例)
- 构建大根堆:
- 从最后一个非叶子节点开始(索引为
n//2 - 1
),对每个节点执行 “下沉” 操作(若节点值小于子节点,与较大子节点交换,直到满足大根堆性质:父节点≥子节点); - 最终数组转化为大根堆,堆顶为最大值。
- 从最后一个非叶子节点开始(索引为
- 排序阶段:
- 将堆顶元素(最大值)与数组末尾元素交换,此时末尾元素为有序部分;
- 缩小堆的范围(忽略已排序的末尾元素),对新堆顶执行 “下沉” 操作,重建大根堆;
- 重复上述步骤,直到堆的范围缩小至 1,数组完全有序。
// 6. 堆排序(不稳定,O(n log n))
public static void heapSort(int[] arr) {
int n = arr.length;
// 构建大顶堆
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(arr, n, i);
}
// 一个个交换元素
for (int i = n - 1; i > 0; i--) {
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
// 重新调整堆结构
heapify(arr, i, 0);
}
}
private static void heapify(int[] arr, int n, int i) {
int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left < n && arr[left] > arr[largest]) {
largest = left;
}
if (right < n && arr[right] > arr[largest]) {
largest = right;
}
if (largest != i) {
int swap = arr[i];
arr[i] = arr[largest];
arr[largest] = swap;
heapify(arr, n, largest);
}
}
示例(数组:[4, 10, 3, 5, 1])
(索引对应关系:根节点为索引 0,左子 = 2i+1,右子 = 2i+2)
构建大根堆:
4 (根节点,索引0)
/ \
10 3 (左子索引1,右子索引2)
/ \
5 1 (左子索引3,右子索引4)
从非叶子节点 10 开始(无需调整),
再处理根节点 4:
根节点 4 的左子 10(>4)、右子 3(<4),与较大的左子 10 交换:
10 (交换后,根节点变为10)
/ \
4 3 (左子变为4,右子仍为3)
/ \
5 1
原左子 4(现在索引 1)的左子 5(>4)、右子 1(<4),与左子 5 交换:
10 (最终大根堆)
/ \
5 3 (左子变为5,满足5≥4和1)
/ \
4 1
(大根堆构建完成)。
排序阶段:
堆顶 10 与末尾 1 交换→[1,5,3,4,10](10 有序);对根节点 1 下沉→[5,4,3,1,10];
堆顶 5 与倒数第 2 位 1 交换→[1,4,3,5,10](5 有序);
对根节点 1 下沉→[4,1,3,1,10]→[4,1,3,1,10]
调整为 [4,1,3,1,10](实际步骤略);
重复后最终有序:[1,3,4,5,10]。
关键特点
- 时间复杂度:
- 构建堆的时间为 O (n),排序阶段每次调整堆为 O (log n),共 n-1 次→总时间复杂度 O(n log n)(最坏、平均、最好情况均为此值)。
- 空间复杂度:O (1),原地排序(仅需常数级额外空间)。
- 稳定性:不稳定(交换堆顶与末尾元素时,可能改变相等元素的相对顺序)。
适用场景
- 对时间复杂度要求高且空间有限的场景(如嵌入式系统、内存紧张的环境);
- 无需稳定性的排序需求(如单纯对数值排序);
- top-k 问题(如从海量数据中找前 k 个最大值,堆排序可高效实现)。
5.插入排序(稳定)(插入)
核心思想:将数组划分为已排序部分和待排序部分,每次选取待排序中的一个数有序的插入到已排序的列表中。
步骤(以升序为例)
- 初始状态:第一个元素视为 “已排序” 部分,其余为 “未排序” 部分。
- 第 1 次插入:取未排序部分的第一个元素(第 2 个元素),与已排序部分的元素从后往前比较,插入到正确位置。
- 重复插入:依次处理未排序部分的每个元素,每次将当前元素插入到已排序部分的对应位置(确保已排序部分始终有序)。
- 结束条件:未排序部分为空,整个数组有序。
// 3. 插入排序(稳定,O(n²))
public static void insertionSort(int[] arr) {
int n = arr.length;
for (int i = 1; i < n; i++) {
int key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
初始:已排序[5],未排序[2, 4, 6, 1, 3]
第1次插入(元素2):
比较2与5(2<5),插入到5左侧 → 已排序[2, 5],未排序[4, 6, 1, 3]
第2次插入(元素4):
比较4与5(4<5)→ 继续比较4与2(4>2),插入到2和5之间 → 已排序[2, 4, 5],未排序[6, 1, 3]
第3次插入(元素6):
比较6与5(6>5),直接插入到5右侧 → 已排序[2, 4, 5, 6],未排序[1, 3]
第4次插入(元素1):
比较1与6(1<6)→ 1<5 → 1<4 → 1<2,插入到2左侧 → 已排序[1, 2, 4, 5, 6],未排序[3]
第5次插入(元素3):
比较3与6 → 3<5 → 3<4 → 3>2,插入到2和4之间 → 已排序[1, 2, 3, 4, 5, 6]
最终排序:[1, 2, 3, 4, 5, 6]
关键特点
- 时间复杂度:
- 最坏情况(逆序数组):O (n²)(需比较和移动大量元素)。
- 最好情况(已排序数组):O (n)(只需遍历一次,无需移动元素)。
- 平均情况:O(n²)。
- 空间复杂度:O (1),原地排序(仅需常数级额外空间)。
- 稳定性:稳定(相等元素插入时会放在原有相等元素的右侧,保持相对顺序)。
6、希尔排序(不稳定)(插入)
(Shell Sort)核心思想:作为插入排序的改进版,通过 “分组” 的方式减少元素移动的距离。先将数组按一定 “步长” 分为多个子数组,对每个子数组用插入排序;逐步缩小步长(最终步长为 1),最后一次按步长 1 执行插入排序,此时数组已接近有序,效率大幅提升。
步骤(以升序为例)
- 选择步长序列:常用步长如
n/2, n/4, ..., 1
(折半步长),或更优的 Knuth 序列(3^k - 1)/2
(如 1, 4, 13, 40...)。 - 按步长分组排序:
- 对每个步长
gap
,将数组分为gap
个子数组(索引 0 与 gap、2gap… 为一组,1 与 1+gap、1+2gap… 为另一组等); - 对每个子数组执行插入排序(子数组内元素已部分有序)。
- 对每个步长
- 缩小步长重复:直到步长为 1,此时数组接近有序,最后一次插入排序效率极高,完成最终排序。
// 7. 希尔排序(不稳定,O(n^1.3))
public static void shellSort(int[] arr) {
int n = arr.length;
// 初始步长为数组长度的一半,逐步减半
for (int gap = n / 2; gap > 0; gap /= 2) {
// 对每个步长进行插入排序
for (int i = gap; i < n; i++) {
int temp = arr[i];
int j;
for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
arr[j] = arr[j - gap];
}
arr[j] = temp;
}
}
}
示例(数组:[12, 34, 54, 2, 3],步长序列 5→2→1)
步长 5:数组长度 5,步长 5 时每组只有 1 个元素,无需排序。
步长 2:
分组:[12,54,3] 和 [34,2];
对第一组插入排序→[3,54,12](原数组对应位置更新为 [3,34,54,2,12]);
对第二组插入排序→[2,34](原数组更新为 [3,2,54,34,12])。
步长 1:对整个数组执行插入排序→[2,3,12,34,54]。
关键特点
- 时间复杂度:
- 依赖步长序列,平均情况约为 O (n^1.3)(折半步长),最优步长下可接近 O (n log²n);
- 最坏情况:O (n²)(如折半步长处理某些特殊数组)。
- 空间复杂度:O (1),原地排序。
- 稳定性:不稳定(分组排序时,相等元素可能被分到不同组,导致相对顺序改变)。
优势与适用场景
- 比插入排序高效:通过大步长减少元素移动次数,尤其对中等规模数据(n=1000~10000)性能较好。
- 实现简单:在插入排序基础上增加步长逻辑即可,适合对性能有一定要求但无需极致优化的场景。
- 不适合对稳定性有要求的场景,也不如归并 / 堆排序适合大规模数据。
7.归并排序(稳定)(归并)
核心思想:基于 “分治” 策略,将数组不断 “拆分” 为两个等长(或近似等长)的子数组,直到子数组长度为 1(天然有序),再通过 “合并” 两个有序子数组的方式,逐步向上整合为一个完整的有序数组。
步骤(以升序为例)
- 拆分阶段:
- 将原数组从中间分为左、右两个子数组;
- 递归拆分左、右子数组,直到每个子数组只有 1 个元素(终止条件)。
- 合并阶段:
- 准备一个临时数组,用于存放合并后的结果;
- 比较两个有序子数组的元素,按从小到大的顺序依次放入临时数组;
- 将临时数组的元素复制回原数组的对应位置,完成合并;
- 递归合并上层子数组,最终得到完整的有序数组。
// 5. 归并排序(稳定,O(n log n))
public static void mergeSort(int[] arr) {
if (arr.length > 1) {
int mid = arr.length / 2;
int[] left = Arrays.copyOfRange(arr, 0, mid);
int[] right = Arrays.copyOfRange(arr, mid, arr.length);
mergeSort(left);
mergeSort(right);
merge(arr, left, right);
}
}
private static void merge(int[] arr, int[] left, int[] right) {
int i = 0, j = 0, k = 0;
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) {
arr[k] = left[i];
i++;
} else {
arr[k] = right[j];
j++;
}
k++;
}
while (i < left.length) {
arr[k] = left[i];
i++;
k++;
}
while (j < right.length) {
arr[k] = right[j];
j++;
k++;
}
}
示例(数组:[8, 4, 5, 7, 1, 3, 6, 2])
拆分:[8,4,5,7] 和 [1,3,6,2] →
继续拆分至 [8]、[4]、[5]、[7]、[1]、[3]、[6]、[2];
合并:
[8] 与 [4] 合并为 [4,8];
[5] 与 [7] 合并为 [5,7];
[4,8] 与 [5,7] 合并为 [4,5,7,8];
同理,右侧子数组合并为 [1,2,3,6];
最终合并 [4,5,7,8] 与 [1,2,3,6] → [1,2,3,4,5,6,7,8]。
关键特点
- 时间复杂度:
- 无论最好、最坏还是平均情况,均为 O(n log n)(拆分次数为 log n 层,每层合并总耗时 O (n))。
- 空间复杂度:O(n)(需额外临时数组存储合并结果,递归调用栈深度为 log n,可忽略)。
- 稳定性:稳定(合并时若两元素相等,会优先放入左侧子数组的元素,保持相对顺序)。
适用场景
- 数据量较大且对时间复杂度要求高的场景(如 n>1000);
- 对稳定性有要求的场景(如排序对象需保持相等元素的原始顺序);
- 外部排序(数据无法全部加载到内存时,归并排序的 “拆分 - 合并” 特性更易实现)。
八、基数排序(分配式排序)
(Radix Sort)核心思想:非比较型排序算法,通过 “按位排序” 实现:从元素的最低位(或最高位)开始,按每位的数值(0-9,或字符 ASCII 码等)将元素分配到对应 “桶” 中,再按桶的顺序收集元素;重复此过程直到所有位处理完毕,数组有序。
步骤(以整数升序、按低位到高位为例)
- 确定最大位数:找到数组中最大元素的位数(如 [123, 45, 6] 的最大位数为 3)。
- 按位排序:
- 从最低位(个位)开始,对每个元素按当前位的数值(不足位数补 0)分配到 0-9 号桶中;
- 按桶的顺序(0→9)收集元素,形成新的数组;
- 依次处理十位、百位…… 直到最高位。
- 结束条件:所有位处理完毕,数组有序。
// 8. 基数排序(稳定,O(d(n+r)))
public static void radixSort(int[] arr) {
if (arr.length == 0) return;
// 找到最大值确定位数
int max = arr[0];
for (int num : arr) {
if (num > max) {
max = num;
}
}
// 对每一位进行排序
for (int exp = 1; max / exp > 0; exp *= 10) {
countingSort(arr, exp);
}
}
private static void countingSort(int[] arr, int exp) {
int n = arr.length;
int[] output = new int[n];
int[] count = new int[10];
// 统计每一位出现的次数
for (int i = 0; i < n; i++) {
count[(arr[i] / exp) % 10]++;
}
// 计算累积次数
for (int i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
// 构建输出数组(从后向前保证稳定性)
for (int i = n - 1; i >= 0; i--) {
output[count[(arr[i] / exp) % 10] - 1] = arr[i];
count[(arr[i] / exp) % 10]--;
}
// 复制回原数组
System.arraycopy(output, 0, arr, 0, n);
}
示例(数组:[170, 45, 75, 90, 802, 24, 2, 66])
- 最大位数:3(802 为 3 位)。
- 按个位排序:
- 个位数值:0(170、90)、2(802、2)、4(24)、5(45、75)、6(66)。
- 收集后:[170, 90, 802, 2, 24, 45, 75, 66]。
- 按十位排序:
- 十位数值:0(802、2)、2(24)、4(45)、6(66)、7(170、75)、9(90)。
- 收集后:[802, 2, 24, 45, 66, 170, 75, 90]。
- 按百位排序:
- 百位数值:0(2、24、45、66、75、90)、1(170)、8(802)。
- 收集后:[2, 24, 45, 66, 75, 90, 170, 802](排序完成)。
关键特点
- 时间复杂度:
- 设元素最大位数为 d,基数为 r(如 10 个桶),则时间复杂度为 *O(d(n + r))**(d 轮分配 + 收集,每轮 O (n + r))。
- 当 d 为常数(如整数最多 32 位)、r≤n 时,可视为 O (n)。
- 空间复杂度:O (n + r)(需 r 个桶存储元素,总容量为 n)。
- 稳定性:稳定(分配到桶时按元素原始顺序放入,收集时保持相对顺序)。
适用场景
- 整数或可按位拆分的类型(如字符串、日期等);
- 数据范围大但位数有限的场景(如手机号、身份证号排序);
- 不适合浮点数(位数不固定)或无法按位比较的类型。
总结
- 简单排序(冒泡、选择、插入):实现简单,适合小规模数据,其中插入排序在基本有序时最优。
- 高级排序(快速、归并、堆、希尔):适合大规模数据,快速排序平均效率最高,归并排序稳定但需额外空间,堆排序空间复杂度低。
- 基数排序:非比较类排序,适合特定场景(如整数),时间效率高但适用范围有限。
十四、MySQL中有一张表,
A:学号 姓名
B:学号 性别
①连表查出来学号,姓名,性别 ?
select A.学号, A.姓名, B.性别
from A ,B where A.学号 = B.学号;
②如果要清空一个表的数据用什么命令?
TRUNCATE TABLE 表名;
truncate(截断,删节) table 表名;
需要完全清空表且不需要回滚 → 使用truncate table
/
DELETE FROM 表名;
清空表数据
需要条件删除或需要事务回滚 → 使用
DELETE FROM
需要清空表但保留自增ID → 使用
DELETE FROM
表有外键约束时 → 通常需要使用
DELETE FROM
,或先禁用外键约束
③更新某个学生的姓名,怎么更新?
update A
set 姓名 = '新姓名'
where 学号 = '学生学号';
④删一个表命令
DROP TABLE 表名;
drop table 表名;
十五、redis是一个什么样的数据库?和别的数据库比它的显著特征?
十六、redis一般作为缓存系统,因为读写速度比较快,那它的缺陷是什么?
十七、计算机网络分层结构,七层,从下往上是什么?
十八、linux关于目录和文件的基本命令?(说十个)
linux常用命令
文件与目录操作
ls
: 列出目录内容(包括文件和子目录)ls -l :(详细信息)
ls -a :包括隐藏文件
cd
-目录名 :进入目录cd .. 返回上一级目录
pwd
- 显示当前工作目录
mkdir
目录名:创建目录
rmdir
目录名:删除空目录rm 文件名:直接删除单个文件
rm -r 目录名: 递归删除目录及其内容
rm -f 目录名:强制删除单个文件
rm -i 文件名:交互式删除(删除前询问确认)
rm -rf / :这将尝试删除系统所有文件(通常会被系统阻止)
rm -rf * :删除当前目录下所有文件
touch 文件名: 创建空文件或更新文件时间戳
cp 文件名 目录:复制文件到目录
cp -r : 递归复制目录
mv 旧文件名 新文件名 :重命名文件
mv file.txt 目录地址 :移动文件
文件内容查看
cat 文件名 :查看文件内容
more/less:分页查看文件内容,less支持向上翻页,适合查看大文件
tail:查看文件末尾若干行
tail -f 用于实时监控内容变化,如日志文件
head:查看文件开头几行
vi/vim 文件名:编辑文件内容
在 Linux 中查看文件内容时,有几种快速查找特定单词/文本的方法:
1. 使用less
查看器(推荐) less 文件名:查找操作:
按/
进入搜索模式,输入要查找的单词,回车
按n
跳转到下一个匹配项
按N
跳转到上一个匹配项
按q
退出 less系统管理:
- ps:显示当前运行的进程列表
- ps aux:查看所有用户进程
- top/htop:实时查看系统重的进程状态和资源使用情况
- kill/killall:终止进程,kill后跟进程id,killall后面跟进程名称
- df:显示文件系统的磁盘使用情况
- du:统计目录或文件占用磁盘空间
- du -h :以人类可读的格式显示
网络配置与调试:
- ping:测试与目标主机的连通性
- ifconfig/ip :查看和配置网络接口信息,ifconfig已经逐渐被 ip 命令替代
- netstat/ss:查看网络连接和端口使用情况,ss是netstat的替代品,提供更详细查询
- curl/wget:发送http请求或下载文件,curl更适合进行API调试,wget则用于下载文件
文件权限和用户管理:
- chmod:修改文件或文件目录,常用模式如 chmod 755
- chown:更改文件或目录所有者
- useradd/userdel:添加或删除用户
- passwd:修改用户密码
用cat查一个小文件,想搜索某个词用什么命令?
cat 文件名
grep "搜索词"
只想查看前十行?
head -n 10 filename
后十行?
tail -n 10 filename
动态查看日志新增内容:tail -f logfile
十九、
①查看一个机器的 ip 地址,想连接一个机器,登录一个机器用什么命令?
查看本机 IP 地址:
ip addr show
登录远程机器:
ssh 用户名@目标IP或域名
# 基本用法
示例:
ssh root@192.168.1.100
ssh user@example.com -p 2222 # 指定非默认端口(22)
②看当前到别的服务器网络通不通用什么命令?
ping 目标IP或域名
示例:
ping 8.8.8.8 # 测试Google DNS
ping example.com # 测试域名解析
③看他的端口号能不能连接上这个用什么命令?
在 Linux 中检查目标服务器的端口是否可连接,常用以下命令:
1.
telnet
(简单测试 TCP 端口)telnet <目标IP或域名> <端口号>
示例:
tenet 113.44.83.92 1208
2.
curl
(测试 HTTP/应用层协议)curl -v http://<目标IP或域名>:<端口> # 测试 Web 服务
curl -v telnet://<目标IP>:<端口> # 测试任意 TCP 端口
示例:
curl -v telnet://113.44.83.92:1208curl -v telnet://113.44.83.92:1208
1. 命令与 URL 处理
curl -v http://113.44.83.92:1208
中,-v
表示输出详细日志(包括请求头、响应头、连接过程等)。* Rebuilt URL to: http://113.44.83.92:1208/
:curl 自动补全了 URL 末尾的/
(标准 HTTP 路径格式)。2. 连接建立过程
* Trying 113.44.83.92...
:开始尝试连接目标 IP113.44.83.92
。* TCP_NODELAY set
:启用 TCP 无延迟选项(减少网络传输延迟)。* Connected to 113.44.83.92 (113.44.83.92) port 1208 (#0)
:成功与目标 IP 的1208
端口建立 TCP 连接。3. 发送 HTTP 请求
> GET / HTTP/1.1
:发送 HTTP GET 请求,路径为/
,协议版本为HTTP/1.1
。> Host: 113.44.83.92:1208
:指定请求的主机和端口(用于服务器识别虚拟主机)。> User-Agent: curl/7.61.1
:告知服务器客户端是curl
工具,版本为7.61.1
。> Accept: */*
:表示客户端接受任意类型的响应数据。4. 服务器响应结果
< HTTP/1.1 401
:服务器返回的 HTTP 状态码为401
,表示 “未授权”(需要身份验证才能访问该资源)。- 响应头信息:
< Vary: Origin
等:服务器告知客户端,响应内容可能因请求的来源、方法、头信息不同而变化。< Content-Length: 0
:响应体长度为 0(无实际内容)。< Date: Wed, 16 Jul 2025 17:10:29 GMT
:服务器处理请求的时间。5. 连接结束
* Connection #0 to host 113.44.83.92 left intact
:请求完成后,TCP 连接保持(未立即关闭)。总结
本次请求的核心结果是:客户端成功与
113.44.83.92:1208
建立连接,但服务器返回401 未授权
,说明访问该地址需要提供用户名和密码等身份验证信息,否则无法获取资源。
二十、域名解析具体是做什么事情?可以举个例子吗?
二十一、前后端协作开发,后端写的接口文档内容有什么?
前后端协作中,后端接口文档通常包含:
以我的博客登录接口为例:/user/login1. 接口基础信息(及描述)
2. 请求参数说明
Body 参数(JSON){ "username": "admin", // 必填,4-20位字符 "password": "a1b2c3d4", // 必填,前端RSA加密后密文 "captcha": "3845" // 可选,登录失败3次后必填 }
字段规则:
password 加密逻辑:前端使用固定公钥MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC... 进行RSA加密
captcha 需调用 /captcha/generate 接口获取
3. 响应体
成功响应(HTTP 200){ "code": 200, "data": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "expire_in": 7200, // token有效期(秒) "user_info": { "user_id": 10001, "username": "admin", "avatar": "https://xxx.com/avatar.jpg" } }, "message": "登录成功" }
失败响应示例
4. 安全控制
请求头要求:http
Authorization: Bearer <token> // 后续接口需携带 X-Request-ID: <uuid> // 请求唯一标识
密码加密流程:javascript
// 前端加密示例(Web) const encryptedPwd = RSA.encrypt('明文密码', '公钥');
5. 调用示例
cURL 请求
bashcurl -X POST 'https://api.example.com/user/login' \ -H 'Content-Type: application/json' \ -d '{ "username": "admin", "password": "a1b2c3d4..." }'
JavaScript Fetch
javascript
fetch('/user/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: 'admin', password: encryptedPwd }) })
6. 辅助信息
Mock 数据(供前端开发使用)
json
{ "code": 200, "data": { "token": "mock_token_123", "user_info": { "user_id": 999, "username": "mock_user" } } }
版本变更记录
7. 注意事项
Token 刷新:Token 过期前调用 /user/token/refresh 续期
错误重试:
连续3次失败后必须要求验证码
日志记录:
所有登录失败会记录IP和设备信息
为什么需要这些细节?
避免联调问题:明确加密规则防止前后端加密方式不一致提高安全性:标注敏感字段处理方式(如密码加密)
快速定位错误:完备的状态码说明减少沟通成本
这样的文档能确保前后端开发者像阅读说明书一样明确交互逻辑。实际开发中推荐使用 Swagger 或 YAPI 自动生成交互式文档。
## 用户登录接口
- URL: /api/v1/login
- Method: POST
- 请求参数:
{
"username": "string",
"password": "string"
}
- 成功响应:
{
"code": 200,
"data": {
"token": "xxxxxx",
"userInfo": {...}
}
}
- 错误响应:
{
"code": 401,
"message": "用户名或密码错误"
}