Java基础
1. 数组如何转为链表?
(1.手动创建链表
然后遍历数组,逐个创建链表节点,并连接它们以构成链表
(2.使用Java集合类
可以使用LinkedList
类构造函数接受Collection
参数,将数组转为链表。
import java.util.LinkedList;
import java.util.List;
public class ArrayToLinkedList {
public static ListNode arrayToLinkedList(int[] arr) {
List<Integer> list = new LinkedList<>();
for (int num : arr) {
list.add(num);
}
ListNode dummy = new ListNode(0);
ListNode current = dummy;
for (int num : list) {
current.next = new ListNode(num);
current = current.next;
}
return dummy.next;
}
}
2. 什么是 hashmap
1.HashMap在Jdk1.8以后是基于数组+链表+红黑树来实现的,特点是,key不能重复,可以为null,线程不安全
2.HashMap的扩容机制:
HashMap的默认容量为16,默认的负载因子为0.75,当HashMap中元素个数超过容量乘以负载因子的个数时,就创建一个大小为前一次两倍的新数组,再将原来数组中的数据复制到新数组中。当数组长度到达64且链表长度大于8时,链表转为红黑树
3.HashMap存取原理:
(1)计算key的hash值,然后进行二次hash,根据二次hash结果找到对应的索引位置
(2)如果这个位置有值,先进性equals比较,若结果为true则取代该元素,若结果为false,就使用高低位平移法将节点插入链表(JDK8以前使用头插法,但是头插法在并发扩容时可能会造成环形链表或数据丢失,而高低位平移发会发生数据覆盖的情况)
在 hashmap 中,如果想要删除某个 key 对应的 value,可以使用 remove( key ) 方法
3. String 是基本数据类型吗
String 不是基本数据类型,而是一个类,实际上是一个引用类型,用于表示字符串
Java八大数据类型:
(1)整数类型:byte、short、int、long
(2)小数类型:float、double
(3)字符类型:char
(4)布尔类型:boolean
4. 如何反转一个字符串
(1.使用 StringBuffer 或 StringBuilder
String original = "Hello";
StringBuilder reversed = new StringBuilder(original).reverse();
String result = reversed.toString();
(2.使用字符数组,依次倒序遍历填入一个新数组中
String original = "Hello";
char[] chars = original.toCharArray();
int left = 0;
int right = chars.length - 1;
while (left < right) {
char temp = chars[left];
chars[left] = chars[right];
chars[right] = temp;
left++;
right--;
}
String result = new String(chars);
(3.Java 8+可以使用Stream
String original = "Hello";
String reversed = new StringBuilder(original).reverse().toString();
5. 如何判断一个字符串中是否含有某个特定的字段
可以使用 contains() 方法
String str = "This is a sample string";
String specificWord = "sample";
if (str.contains(specificWord)) {
System.out.println("字符串包含特定的字段");
} else {
System.out.println("字符串不包含特定的字段");
}
6. 什么是迭代器
迭代器是一种对象,用于遍历集合中的元素,提供了一种统一的访问集合元素的方式,而不暴露集合的内部实现细节。通过迭代器,你可以依次访问集合中的每个元素,判断是否有下一个元素,并获取当前元素的值。
在Java中,迭代器通常是通过Iterator
接口来实现的。这个接口提供了一系列用于遍历集合的方法,如hasNext()
(检查是否有下一个元素)、next()
(获取下一个元素)、remove()
(从集合中移除当前元素)等。通过调用集合类的iterator()
方法,你可以获取到对应集合的迭代器,然后使用它来遍历集合的元素。
List<String> list = new ArrayList<>();
list.add("apple");
list.add("banana");
list.add("orange");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(element);
}
7. 如何希望一个集合不被修改,可以怎么做
(1. 不对外暴露相关接口
(2.使用 final 关键字修饰
(3.使用 Collections 中的相关方法,如Collections.unmodifiableList()
、Collections.unmodifiableSet()
、Collections.unmodifiableMap()
List<String> readOnlyList = Collections.unmodifiableList(originalList);
8. 如何使用IO流读取文件
可以使用 FileInputStream 或者 BufferReader 等
(1.使用 FileInputStream 逐个字节读取文件
import java.io.*;
public class ReadFile {
public static void main(String[] args) {
try {
File file = new File("path/to/your/file.txt");
FileInputStream fis = new FileInputStream(file);
int content;
while ((content = fis.read()) != -1) {
// 处理文件内容
System.out.print((char) content);
}
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
(2.使用 BufferReader 按行读取文件
import java.io.*;
public class ReadFile {
public static void main(String[] args) {
try {
File file = new File("path/to/your/file.txt");
BufferedReader br = new BufferedReader(new FileReader(file));
String line;
while ((line = br.readLine()) != null) {
// 处理每一行内容
System.out.println(line);
}
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
9. http与https
HTTP(超文本传输协议)和HTTPS(安全超文本传输协议)是用于在网络上传输数据的协议,它们之间的主要区别在于安全性和数据传输方式:
安全性:
- HTTP:数据以纯文本形式传输,不加密。因此,数据容易被窃取或篡改,安全性较低。
- HTTPS:通过使用SSL(安全套接层)或TLS(传输层安全)协议对数据进行加密和认证,确保传输过程中的安全性。因此,HTTPS比HTTP更加安全。
传输方式:
- HTTP:默认使用端口80进行通信。
- HTTPS:默认使用端口443进行通信。
加密方式:
- HTTP:数据未经加密传输。
- HTTPS:数据经过加密传输,对数据进行了加密处理,使得中间人无法直接读取其中的内容。
安全标识:
- HTTP:不提供安全性认证机制,无法对服务器进行验证。
- HTTPS:浏览器地址栏会显示一个锁形状的图标或其他安全标志,表明连接是安全的。
总的来说,HTTPS是HTTP的安全版本,通过加密通信内容确保了数据传输的安全性。
10. 什么是TCP协议
TCP(传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层协议,它负责在网络中可靠地传输数据。以下是关于TCP协议的主要特点:
可靠性:TCP提供可靠的数据传输机制。它通过使用确认、重传和超时机制来确保数据可靠地到达目的地。
面向连接:在数据传输之前,TCP需要建立一个连接。连接建立包括三个步骤:三次握手(SYN,SYN-ACK,ACK)以确保通信双方都能发送和接收数据。
流式传输:TCP是一种面向字节流的协议,它将数据视为连续的字节流,而非数据包的集合。
拥塞控制:TCP通过拥塞控制算法避免网络拥塞,当网络出现拥塞时,TCP会自动调整数据传输的速率。
双向通信:TCP连接是全双工的,意味着在一个TCP连接上,数据可以双向传输。
可靠的数据顺序传输:TCP保证了数据包按照发送的顺序到达接收端,即使在网络中发生了乱序也会将数据包重新排序。
三次握手:
11. 前端如何向后端发送请求
AJAX: 使用JavaScript中的AJAX(Asynchronous JavaScript and XML)技术,通过XMLHttpRequest对象发送HTTP请求,与后端进行数据交互。现在更常用的是基于Fetch API的方式。
WebSocket: WebSocket允许双向通信,前端可以通过WebSocket与后端建立持久连接,进行实时通信。
Form提交: 通过HTML的表单元素,使用POST或GET方式提交数据到后端。
前端框架内置方法: 许多现代前端框架(如React、Angular、Vue等)提供了内置的方法,例如Axios、Fetch等库,用于方便地向后端发送请求。
RPC(Remote Procedure Call): 通过远程过程调用方式,在前端直接调用后端提供的服务方法。
12. Get与Post请求有什么区别
- HTTP Method:@GetMapping处理HTTP GET请求,而@PostMapping处理HTTP POST请求。
- 参数传递方式:@GetMapping从URL路径中获取参数,@PostMapping从请求体中获取参数。
- 安全性:POST请求比GET请求更安全,因为GET请求将参数暴露在URL中,所以不建议使用GET请求来处理敏感数据。
- RESTful API设计:@GetMapping通常用于查询资源,而@PostMapping通常用于创建或更新资源。
13. 为什么要使用消息队列MQ
解耦和异步通信: 消息队列允许不同部分的应用程序进行解耦,发送者和接收者之间不需要直接交互。这种解耦性允许系统的不同部分能够独立地扩展和修改而不影响其他部分。同时,消息队列还支持异步通信,发送者发送消息后就可以继续自己的工作,而无需等待接收者的响应。
缓冲和削峰填谷: 消息队列可以作为一个缓冲层,当系统的处理速度不足以跟上数据产生的速度时,消息队列可以缓存数据并逐步处理。这可以防止数据丢失或者系统崩溃。同样,它可以应对突发流量,将高峰期的请求存储在队列中逐渐处理,以平滑系统负载。
可靠性和持久性: 很多消息队列提供持久化选项,确保即使在应用程序或服务器崩溃的情况下,消息也不会丢失。这增加了系统的可靠性,因为即使系统出现问题,数据也不会丢失。
跨系统通信: 消息队列允许不同系统之间进行通信,这些系统可能使用不同的编程语言或部署在不同的平台上。消息队列提供了一种标准的通信方式。
14. 有哪几种情况会导致消息丢失
不可靠的消息代理: 某些消息队列的部署或配置可能不够稳定,导致消息代理(例如,Kafka、RabbitMQ)本身出现故障或错误。这可能会导致消息丢失。
消息超时: 消息队列中的消息可能会设置超时时间。如果消息在超时时间内没有被消费,消息可能会被视为过期并被丢弃。
未持久化的消息: 如果消息队列未配置为持久化存储消息,在服务器故障或重启时可能会导致消息丢失。
错误的消费者行为: 消费者在处理消息时发生错误,并且未正确地处理失败的情况,可能导致消息丢失。
并发或竞争条件: 在多个消费者同时处理消息的情况下,竞争条件可能导致消息重复消费或丢失。
15. 消费者故障导致消息丢失该如何处理?
消息确认机制: 在一些消息队列系统中,消息在被消费者处理之前不会被删除。在确认机制下,如果消费者成功处理消息,可以发送确认,否则可以触发消息的重新投递或者重试机制。
死信队列: 一些消息队列系统支持死信队列,当消息被消费者处理失败或超时时,消息可以被发送到死信队列。这样可以将无法处理的消息放置在专门的队列中进行处理或分析。
---消费者↑---------------通用↓------------------------
重试策略: 在消费者处理消息时,可能会出现一些临时问题,例如网络中断、资源不足等。通过设置适当的重试策略,可以在消费者失败后重新尝试处理消息。
持久化和事务: 使用消息队列的持久化功能,确保消息在传递过程中即使在系统故障或重启时也不会丢失。此外,使用事务机制能够保证消息的可靠传递和处理。
监控和告警系统: 配置监控系统以实时监控消息队列的状态,以及可能出现的异常情况,设置告警以及时发现问题并进行处理。
16. 常见的HTTP状态码
1xx(信息性状态码):指示请求正在处理。
2xx(成功状态码):指示请求被成功接收、理解和接受。
- 200(OK):请求成功。
- 201(Created):请求已经被实现,而且有一个新的资源已经依据请求的需要而建立。
3xx(重定向状态码):需要客户端进行进一步操作以完成请求。
- 301(Moved Permanently):永久性重定向,请求的资源已被分配了新的 URI。
- 302(Found):临时性重定向,请求的资源临时被分配了新的 URI。
- 304(Not Modified):客户端有缓存的文档并发出了一个条件性的请求。
4xx(客户端错误状态码):表示客户端出错。
- 400(Bad Request):请求报文存在语法错误。
- 403(Forbidden):服务器拒绝请求。
- 404(Not Found):请求的资源不存在。
5xx(服务器错误状态码):表示服务器出错。
- 500(Internal Server Error):服务器遇到了不知道如何处理的情况。
- 503(Service Unavailable):服务器暂时无法处理请求,通常是因为维护或过载。
17.==与equals
-
==运算符:
- ==运算符用于比较两个对象的引用是否相同,即比较两个对象在内存中的地址是否相同。如果两个对象的引用指向的是同一个内存地址,则返回true,否则返回false。
- 对于基本数据类型,==运算符比较的是值是否相等。
- 对于引用类型,==运算符比较的是对象的引用是否相同。
-
equals方法:
- equals方法是Object类中的方法,在Java中的所有类都继承自Object类,因此所有对象都可以调用equals方法。
- 默认情况下,Object类中的equals方法与==运算符的作用是一样的,即比较两个对象的引用是否相同。
- 但是,很多类(如String、Integer等)都重写了Object类中的equals方法,改变了其默认行为,使其比较的是对象的内容是否相同。
18.list与map的异同
-
结构不同:
- List是有序集合,它按照元素插入的顺序来维护元素的顺序。可以通过索引来访问列表中的元素。
- Map是键值对的集合,每个元素包含一个键和一个值。Map中的键是唯一的,值可以重复。Map中的元素是无序的,即不保证元素的插入顺序。
-
存储方式不同:
- List以单个元素的形式存储元素,每个元素可以通过索引来访问。
- Map以键值对的形式存储元素,每个元素由键和值组成,通过键来访问对应的值。
-
实现接口不同:
- List接口的主要实现类有ArrayList、LinkedList和Vector等。
- Map接口的主要实现类有HashMap、TreeMap、LinkedHashMap和Hashtable等。
-
适用场景不同:
- List适用于需要按照顺序存储元素,并且可能需要频繁访问元素的场景。
- Map适用于需要存储键值对,并且可能需要根据键快速查找值的场景。
在一般情况下,Map中的元素是无序的,即不保证元素的插入顺序。这是因为Map是基于键值对的存储结构,它的实现类通常是通过哈希表来实现的,而哈希表是一种无序的数据结构。
但是,有一种特殊的Map实现类LinkedHashMap,它可以保留元素插入的顺序。LinkedHashMap维护了一个双向链表来记录元素的插入顺序,因此它可以保证元素的顺序与插入顺序一致。当需要保留元素插入顺序时,可以使用LinkedHashMap来实现。
19.面向对象的三大特征
-
封装(Encapsulation):
- 封装是指将数据(属性)和操作数据的方法(行为)封装在一个类中,并对外部隐藏对象的内部实现细节,只暴露必要的接口供外部访问。
- 封装可以提高代码的安全性和可维护性,避免了直接访问对象内部数据的可能性,使对象的状态更加可控和可靠。
-
继承(Inheritance):
- 继承是指一个类(子类)可以继承另一个类(父类)的属性和方法,并且可以在不改变父类的情况下进行扩展或修改。
- 继承可以实现代码的重用,减少了重复编码,提高了代码的可维护性和可扩展性。
-
多态(Polymorphism):
- 多态是指同一个操作作用于不同的对象上会产生不同的行为,即同一个方法可以在不同的对象上表现出不同的行为。
- 多态可以通过方法重写(Override)和方法重载(Overload)来实现。方法重写是子类重写父类的方法,而方法重载是在同一个类中定义多个同名方法,但参数列表不同。
- 多态提高了代码的灵活性和可扩展性,使代码更加通用和易于维护。
20.Java内存模型
Java内存模型一般分为:方法区、堆、虚拟机栈、程序计数器、本地方法栈
1.方法区
- Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的方法区。
- 在虚拟机启动时创建
- jvm8之后,方法区的实现发生了变化,被称为“元空间”(Metaspace)。元空间不再是固定大小的,它位于本地内存之中,而不是jvm内存中。它可以根据需要动态扩展,而不会导致OutOfMemoryError。
2.堆(heap)
- 通过new关键字,创建对象都会使用堆内存
- 它是线程共享的,堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制
3.虚拟机栈
- 每个线程运行时所需要的内存,成为虚拟机栈
- 每个栈由多个栈帧(Frame)组成,对应每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应当前正在执行的那个方法
4.程序计数器
- 作用:记住下一条jvm指令的执行地址
- 是线程私有的,即只服务于一个线程
- 不会存在内存溢出
- 轻量级,占用的内存非常小,通常只有几个字节
- 随着线程创建而创建,随着线程销毁而销毁
5.本地方法栈
- 用于管理调用本地方法(Native Method)的过程
- 本地方法是指由本地(非Java)语言编写的方法,通常使用JNI(Java Native Interface)来与Java代码进行交互。
- 本地方法通常是用C、C++等本地编程语言编写的,它们无法像Java方法那样直接运行在JVM上,因此需要本地方法栈来管理调用过程。
21.Java垃圾回收机制
1.如何判断垃圾可回收
1.1.引用计数法
这个算法跟踪每个对象的引用计数。当引用计数达到零时,对象就被认为是垃圾。引用计数算法容易实现,但它无法处理循环引用的情况,并且会产生额外的开销来维护引用计数。
1.2.可达性分析算法
可达性分析算法(Reachability Analysis)是垃圾回收领域中的一种算法,用于确定在程序执行过程中哪些对象是可访问(或可达)的,以及哪些对象不可访问,从而确定哪些对象应该被回收。
过程:扫描堆中的对象,看是否能够沿着 GC Root 对象 为起点的引用链找到该对象,找不到,表示可以回收
优点:
- 能够处理循环引用(对象之间相互引用)的情况,不会将仍然相互引用的对象误判为垃圾。
- 能够精确识别可达对象,确保不会回收仍然被引用的对象。
- 适用于各种内存分配模式和数据结构,包括复杂的数据结构和图形数据。
2.垃圾回收算法
1.标记清除算法
- 定义:这是最基本的垃圾回收算法之一。它分为两个阶段:标记阶段和清除阶段。在标记阶段,垃圾回收器标记所有仍然存活的对象。在清除阶段,垃圾回收器删除未标记的对象。
- 优点:速度快
- 缺点:标记-清除算法有一个缺点,即会产生内存碎片。
2.标记整理算法
- 定义:这个算法结合了标记-清除和复制算法的思想。首先,标记阶段标记所有存活的对象。然后,所有存活的对象会被移动到一起,以便在堆内存中形成连续的块,然后清除未被移动的对象。这减少了内存碎片,但需要额外的复制操作。
- 优点:不会产生内存碎片
- 缺点:速度较慢
3.复制算法
- 定义:这个算法将堆内存分为两个区域,通常称为"From"和"To"。对象首先分配在From区域,当From区域满时,垃圾回收器会将仍然存活的对象复制到To区域,并清除From区域。
- 不会产生内存碎片
- 需要占用双倍内存空间
3.垃圾回收器
1.串行垃圾回收器
工作流程如上:串行是一种单线程的垃圾回收器。当某个线程需要进行垃圾回收时,所有线程都会在一个安全点内暂停,待垃圾回收结束之后再继续运行
安全点是指,当线程运行到这类位置时,堆对象状态是确定一致的,JVM可以安全地进行操作,如GC,偏向锁解除等。
2.吞吐量优先
吞吐量优先垃圾回收器是并行工作机制,即会有多个垃圾回收线程同时运行,期间其他线程暂停。
3.响应时间优先
响应时间优先是并发机制,即垃圾回收线程运行时,其他工作线程也能正常运行
初始标记阶段:这个阶段会标记那些仍然存活的对象。标记所有 根对象。这个阶段会造成STW(也有文章说不会造成STW)
并发标记阶段:用户线程恢复运行,与此同时,我们的垃圾回收线程它还可以继续并发标记,把剩余的那些垃圾对象(因为上次标记只标记了根对象)给它找出来,这里跟用户线程是并发执行的,在这个过程中,它不用 STW,所以它的响应时间是很短的
重新标记阶段:这个阶段会再次触发STW,这是因为在并发标记的同时, 用户线程也在工作,它工作的时候就有可能产生一些新的对象,改变一些对象的引用,就可能对垃圾回收做了一些干扰,因此会再次标记垃圾对象
再次清理阶段:这个阶段会清理并发标记阶段产生的新的垃圾对象,因为是并发运行垃圾回收线程,所以不会造成STW。不过因为这个阶段用户线程也在同时运行,所以有可能产生新的垃圾,称为“浮动垃圾”
4.G1垃圾回收器
22.常用的linux命令有哪些
- 文件目录
- pwd 查看当前目录
- ls或ll 查看目录下的文件和子目录
- cd 切换目录
- mkdir 创建目录
- mv 移动文件/目录
- rm 删除文件/目录
- chmod 修改文件或目录的权限
- 文件内容
- cat 查看文件内容
- vim 编辑文件
- find 搜索文件
- grep 搜索文件内容
- 防火墙相关
- systemctl status firewalld.service 查看防火墙状态
- systemctl start firewalld.service 开启防火墙
- systemctl stop firewalld.service 关闭防火墙
- systemctl restart firewalld.service 重启防火墙
- firewall-cmd –query-port=8080/tcp 查询端口是否开放
- firewall-cmd –permanent –add-port=8080/tcp 开放指定端口
- firewall-cmd –permanent –remove-port=8080/tcp 移除开放的端口
- 进程相关
- ps -ef | grep java 显示出所有的java进程
- kill 杀掉指定的进程
23.为什么缓存使用redis而不是别的
-
性能高:Redis是基于内存的缓存数据库,读写速度非常快,可以达到每秒数十万次的读写操作,适用于高并发场景。
-
支持丰富的数据类型:Redis支持多种数据类型,包括字符串、哈希、列表、集合、有序集合等,这些数据类型的灵活性能够满足不同场景的需求。
-
持久化支持:Redis支持多种持久化方式,可以将数据保存到磁盘上,以防止数据丢失。
-
分布式支持:Redis支持主从复制和集群模式,可以搭建高可用性的分布式缓存系统,提高系统的稳定性和扩展性。
-
丰富的功能:Redis除了作为缓存之外,还提供了丰富的功能,例如发布订阅、事务支持、Lua脚本等,可以满足不同场景下的需求。
-
社区活跃:Redis拥有庞大的开源社区支持,有大量的文档、教程和社区贡献的插件,可以帮助开发者快速解决问题。
24.模块化开发的优缺点是什么
优点:
-
可维护性增强:模块化开发使得软件系统更易于理解和维护。每个模块都专注于解决特定问题,使得代码更具可读性、可维护性和可扩展性。
-
开发效率提高:模块化开发允许团队成员并行开发不同的模块,从而加快了开发速度。模块化设计也使得代码重用更加容易,可以减少重复开发工作。
-
测试容易:模块化开发使得单元测试更加容易实施,因为每个模块都可以单独测试。这有助于提高代码质量和稳定性,并减少故障排查的时间。
-
降低耦合度:模块化开发通过定义清晰的接口和依赖关系,降低了模块之间的耦合度。这样可以减少模块间的依赖关系,提高了系统的灵活性和可维护性。
-
提高复用性:模块化开发使得模块更易于被其他系统或项目复用,从而提高了代码的复用率和开发效率。
缺点:
-
模块间通信成本:模块化开发需要模块之间进行通信,可能会增加一些额外的开销和复杂性。特别是在分布式系统中,模块间的通信可能涉及网络通信,增加了延迟和失败的风险。
-
划分不当导致问题:如果模块划分不合理或者接口设计不良,可能会导致模块间的依赖关系复杂、耦合度高,进而影响系统的灵活性和维护性。
-
管理复杂性增加:模块化开发需要对模块进行规划、设计、测试、部署和维护,这增加了项目管理的复杂性和成本。
-
版本管理困难:如果模块之间存在复杂的依赖关系,可能会导致版本管理困难。特别是在大型系统中,不同模块的版本可能不一致,需要仔细管理和协调。
25.nacos注册中心是如何发现服务的?
当服务启动时,它会向Nacos注册中心注册自己的信息,包括服务名称、IP地址、端口号等。其他服务想要调用这个服务时,它们会向Nacos注册中心发送查询请求,询问某个特定服务的地址信息。Nacos注册中心收到请求后,会返回该服务的所有实例信息,包括它们的IP地址和端口号等。然后调用方就可以根据这些信息去调用目标服务。
在基于Spring Cloud的微服务架构中,通常会使用Spring Cloud Netflix的组件,比如Eureka作为注册中心。在这种情况下,服务启动时,Spring Boot应用会自动注册到Eureka注册中心,而无需额外的配置。
26.冒泡算法和快排的原理
冒泡:
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
- 比较相邻的两个元素。如果第一个比第二个大(或小),则交换它们的位置。
- 对每一对相邻元素重复上述步骤,从数组的开始位置到倒数第二个元素,这样一轮比较交换完成后,数组的最后一个元素就是最大(或最小)的元素。
- 针对剩下的未排序元素,重复以上步骤,直到整个数组排序完成。
快排:
public static void quickSort(int nums[], int start, int end) {
//数组有多个元素进行排序
if (start < end) {
int base = nums[start];//以要进行排序数组第0个元素为base
int left = start;//左指针
int right = end;//右指针
while (left < right) {
//从右向左找,比base大,right--
while (left< right && nums[right] >= base) {
right--;
}
//比base小,替换left所在位置的数字
nums[left] = nums[right];
//从左向右找,比base小,left++
while (left < right && nums[left] <= base){
left++;
}
//比base大,替换right所在位置的数字
nums[right] = nums[left];
}
nums[left] = base;//此时left=right,用base替换这个位置的数字
//排列比base小的数字的数组
quickSort(nums, start, left - 1);
//排列比base大的数字的数组
quickSort(nums, left + 1, end);
}
}
- 选择基准元素:从数组中选择一个基准元素(通常选择第一个元素)。
- 分割数组:将数组分割成两个子数组,左边的子数组包含所有小于等于基准元素的元素,右边的子数组包含所有大于等于基准元素的元素。基准元素在这一步已经找到了它在排序后的最终位置,通常称为分区操作。
- 递归排序:对左右两个子数组递归地应用快速排序算法,直到每个子数组只有一个元素或为空。
- 合并结果:由于在分割数组的过程中,基准元素已经找到了它的最终位置,因此整个数组已经排好序了。
27.常见数据结构
1.顺序表(数组):
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表一般可以分为:
- 静态顺序表:使用定长数组存储。
- 动态顺序表:使用动态开辟的数组存储。
2.链表:
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
3.栈:
一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除叫做出栈,出数据在栈顶。
4.队列
只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出 FIFO(First In First Out)
入队列:进行插入操作的一端称为队尾
出队列:进行删除操作的一端称为队头
5.树
树是一种数据结构,它是由n(n>=1)个有限节点组成一个具有层次关系的集合。把它叫做 “树” 是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:
每个节点有零个或多个子节点;
没有父节点的节点称为根节点;
每一个非根节点有且只有一个父节点;
除了根节点外,每个子节点可以分为多个不相交的子树;
二叉树是树的特殊一种,具有如下特点:
- 每个结点最多有两颗子树,结点的度最大为2。
- 左子树和右子树是有顺序的,次序不能颠倒。
- 即使某结点只有一个子树,也要区分左右子树。
完全二叉树:叶子节点只会出现在最后2层,且最后一层的叶子节点都靠左对齐。
满二叉树:所有节点的度要么为0,要么为2,且所有的叶子节点都在最后一层。
满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树
6.堆
堆是一种比较特殊的数据结构,可以被看做一棵树的数组对象,具有以下的性质:
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树。
将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。常见的堆有二叉堆、斐波那契堆等。
7.图
图(Graph)是由节点(或顶点)和边组成的一种数据结构。
-
节点(顶点):图中的节点是图的基本单位,通常表示某种实体或对象。节点可以包含任意类型的数据,例如人的姓名、城市的名称等。在图中,节点通常用圆圈或方框表示。
-
边:图中的边是节点之间的连接关系,用来表示节点之间的关联或联系。边可以是有向的(即有方向的),也可以是无向的(即没有方向的)。有向边通常表示单向关系,例如A指向B;无向边通常表示双向关系,例如A和B之间的友谊关系。边可以有权重,用来表示节点之间的关联程度或距离。
根据节点和边之间的关系,图可以分为以下几种类型:
-
有向图(Directed Graph):图中的边是有方向的,即从一个节点指向另一个节点的关系。例如,社交网络中的“关注”关系就是一个有向图。
-
无向图(Undirected Graph):图中的边是无方向的,即没有明确的起点和终点。例如,城市之间的道路网络就是一个无向图。
-
加权图(Weighted Graph):图中的边具有权重,表示节点之间的关联程度或距离。例如,地图中的道路网络就可以用加权图来表示,边的权重表示道路的长度或行驶时间。
-
连通图(Connected Graph):图中的任意两个节点之间都存在路径,即图中没有孤立的节点。例如,社交网络中所有用户之间都存在关系的图就是连通图。
-
无环图(Acyclic Graph):图中不存在环路,即不存在从一个节点出发经过若干条边回到自身的路径。例如,课程选修中的先修关系图就是一个无环图。
28.多线程的几种方式
- 继承Thread类:通过继承Thread类并重写其run()方法来创建线程。然后通过创建Thread类的实例对象并调用start()方法来启动线程。
- 实现Runnable接口:通过实现Runnable接口并实现其run()方法来创建线程。然后将实现了Runnable接口的类作为参数传递给Thread类的构造方法创建Thread对象,并调用start()方法启动线程。
- 使用匿名内部类:可以在创建Thread对象时直接使用匿名内部类实现Runnable接口或重写Thread类的run()方法。
- 使用线程池:通过Executor框架提供的线程池来创建和管理线程,可以重用线程、控制并发线程数量、提高性能和资源利用率。
29.
SQL基础
1. 死锁的四个条件是什么
-
互斥条件: 一个资源每次只能被一个进程使用。如果一个进程持有了一个资源,其他进程就无法访问该资源,直到它被释放。
-
不可剥夺条件: 进程获得的资源在未使用完毕之前,不能被其他进程抢占,只能由持有资源的进程自己释放。
-
请求与保持条件: 已经得到一部分资源的进程可以继续请求新的资源。如果其他资源被占用,请求进程会等待,但它不会释放自己已有的资源。
-
循环等待条件: 一组进程互相持有其他进程所需的资源,导致形成一个环路,每个进程都在等待其他进程释放资源。
2. SQL中如何获取当前时间
可以使用 NOW() 函数来获取当前日期与时间,另外,CURDATE() 函数会返回当前日期,CURTIME() 函数会返回当前时间
3. group by 的原理是什么
explain select city ,count(*) as num from staff group by city;
- 创建内存临时表,表里有两个字段 city 和 num;
- 全表扫描 staff 的记录,依次取出 city = ‘X’ 的记录。
- 判断 临时表 中是否有为 city=‘X’ 的行,没有就插入一个记录 (X,1);
- 如果临时表中有 city=‘X’ 的行,就将 x 这一行的 num 值加 1;
- 遍历完成后,再根据字段 city 做排序,得到结果集返回给客户端。
4. 索引为什么能加快查询
减少数据扫描: 索引可以帮助数据库引擎快速定位到存储数据的位置,减少了需要扫描的数据量。相当于是建立了一种快速访问数据的路径。
排序和聚合: 索引可以加速排序和聚合操作。当查询包含排序或聚合函数时,索引可以帮助数据库直接获取有序的数据或者通过索引快速完成聚合计算。
加速连接操作: 在连接多个表进行查询时,索引可以加速连接操作。如果连接的字段有索引,数据库可以更快地定位到匹配的行,减少连接所需的时间。
优化唯一性约束: 对于唯一性约束的字段建立索引,可以确保数据的唯一性,同时保证快速的唯一性检查。
5. 什么情况会导致索引失效
- 索引列运算
- 不要在索引列上进行运算操作, 索引将失效。
- 字符串不加引号
- 字符串类型字段使用时,不加引号,索引将失效。
- 模糊查询
- 如果仅仅是尾部模糊匹配,索引不会失效。如果是头部模糊匹配,索引失效
- or连接条件
- 用or分割开的条件, 如果or前的条件中的列有索引,而后面的列中没有索引,那么涉及的索引都不会被用到,左右两侧字段都有索引时,索引才会生效。
- 数据分布影响
- 如果MySQL评估使用索引比全表扫描更慢,则不会使用索引
- 最左前缀法则
- 最左前缀法则指的是查询从索引的最左列开始, 并且不跳过索引中的列。如果跳跃某一列,索引将会部分失效( 后面的字段索引失效 ) 。
- 联合索引中,出现范围查询 (>,<) ,范围查询右侧的列索引失效。
6.表的设计原则
- 命名规范
- 优先考虑逻辑删除而不是物理删除
- 尽可能使用 not null 定义字段
- 首先,
NOT NULL
可以防止出现空指针问题。 - 其次,
NULL
值存储也需要额外的空间的,它也会导致比较运算更为复杂,使优化器难以优化SQL。 NULL
值有可能会导致索引失效- 如果将字段默认设置成一个空字符串或常量值并没有什么不同,且都不会影响到应用逻辑, 那就可以将这个字段设置为
NOT NULL。
- 首先,
- 不需要严格遵循三范式
- 三范式
- 第一范式:对属性的原子性,要求属性具有原子性,不可再分解;
- 第二范式:对记录的唯一性,要求记录有唯一标识,即实体的唯一性,即不存在部分依赖;
- 第三方式:对字段的冗余性,要求任何字段不能由其他字段派生出来,它要求字段没有冗余,即不存在传递依赖;
- 三范式
- 避免外键和级联
- 使用外键存在性能问题、并发死锁问题、使用起来不方便等等。每次做DELETE或者UPDATE都必须考虑外键约束,会导致开发的时候很难受,测试数据造数据也不方便。
- 还有一个场景不能使用外键,就是分库分表
7.一张表查询特别慢该如何优化
-
索引优化:
- 确保表中的字段都有合适的索引,特别是经常被查询的字段。使用索引可以大大提高查询速度。但是要注意,过多的索引也会影响写入性能,因此需要权衡。
-
SQL优化:
- 优化查询语句,确保查询语句简洁高效。避免使用SELECT * 查询所有字段,而是只查询需要的字段;避免使用不必要的JOIN操作等。
-
分区和分表:
- 如果表的数据量很大,可以考虑将表进行分区或分表,将数据按照一定的规则拆分到不同的物理存储位置,以提高查询效率。
-
硬件优化:
- 考虑提升硬件配置,例如增加内存、优化磁盘读写速度、使用更高性能的CPU等,以提升整体系统的性能。
-
缓存优化:
- 使用缓存技术,将经常被查询的数据缓存起来,减少数据库查询压力,提高响应速度。可以使用内存缓存、分布式缓存等。