doc 新增数据结构分析课常考知识点文档
10
.idea/.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# Zeppelin ignored files
|
||||
/ZeppelinRemoteNotebooks/
|
36
.idea/inspectionProfiles/Project_Default.xml
Normal file
@ -0,0 +1,36 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="JavaDoc" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="TOP_LEVEL_CLASS_OPTIONS">
|
||||
<value>
|
||||
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
|
||||
<option name="REQUIRED_TAGS" value="" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="INNER_CLASS_OPTIONS">
|
||||
<value>
|
||||
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
|
||||
<option name="REQUIRED_TAGS" value="" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="METHOD_OPTIONS">
|
||||
<value>
|
||||
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
|
||||
<option name="REQUIRED_TAGS" value="@return@param@throws or @exception" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="FIELD_OPTIONS">
|
||||
<value>
|
||||
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
|
||||
<option name="REQUIRED_TAGS" value="" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="IGNORE_DEPRECATED" value="false" />
|
||||
<option name="IGNORE_JAVADOC_PERIOD" value="true" />
|
||||
<option name="IGNORE_DUPLICATED_THROWS" value="false" />
|
||||
<option name="IGNORE_POINT_TO_ITSELF" value="false" />
|
||||
<option name="myAdditionalJavadocTags" value="date" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
9
.idea/learning_record_doc.iml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
8
.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/learning_record_doc.iml" filepath="$PROJECT_DIR$/.idea/learning_record_doc.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
3
.idea/sonarlint/issuestore/index.pb
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
l
|
||||
<web/docker-compose一键部署蘑菇博客设置SSL证书.md,0\f\0fd96cadf695f65b8aeed80166a160b5319a252d
|
6
.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
12
apps/wg/docker安装.md
Normal file
@ -0,0 +1,12 @@
|
||||
```shell
|
||||
$ wg genkey
|
||||
aMz2AWIQIkyAUhs7b7UU7fEBHCeD3OBvMJFSfaNvq34=
|
||||
|
||||
$ docker run -it -d --name wg --cap-add NET_ADMIN --device /dev/net/tun:/dev/net/tun -v wg-access-server-data:/data -e "WG_ADMIN_PASSWORD=ds4810" -e "WG_WIREGUARD_PRIVATE_KEY=aMz2AWIQIkyAUhs7b7UU7fEBHCeD3OBvMJFSfaNvq34=" -p 8000:8000/tcp -p 51820:51820/udp place1/wg-access-server
|
||||
|
||||
|
||||
#客户端
|
||||
sudo mv wg.conf /etc/wireguard/
|
||||
wg-quick up wg
|
||||
```
|
||||
|
2
apps/阿里云/阿里云密钥.md
Normal file
@ -0,0 +1,2 @@
|
||||
LTAI5tKPzZeKE6WAe2kM2pJE
|
||||
aSmNeWpisOtGJAh1nvS05zUaDIxecr
|
@ -84,12 +84,35 @@ $ sudo rm -rf /var/lib/containerd
|
||||
## Expand
|
||||
|
||||
#### 1.配置镜像加速器
|
||||
|
||||
下面列出国内常用的加速站点,排名不分先后,总体来说阿里云速度较稳定。
|
||||
|
||||
docker中国区官方镜像加速:
|
||||
|
||||
[https://registry.docker-cn.com](https://link.zhihu.com/?target=https%3A//registry.docker-cn.com/)
|
||||
|
||||
网易镜像加速:
|
||||
|
||||
[http://hub-mirror.c.163.com](https://link.zhihu.com/?target=http%3A//hub-mirror.c.163.com/)
|
||||
|
||||
中国科技大学镜像加速:
|
||||
|
||||
[https://docker.mirrors.ustc.edu.cn](https://link.zhihu.com/?target=https%3A//docker.mirrors.ustc.edu.cn/)
|
||||
|
||||
腾讯云镜像加速:
|
||||
|
||||
[https://mirror.ccs.tencentyun.com](https://link.zhihu.com/?target=https%3A//mirror.ccs.tencentyun.com/)
|
||||
|
||||
阿里云镜像加速:
|
||||
|
||||
[https://ung2thfc.mirror.aliyuncs.com](https://link.zhihu.com/?target=https%3A//ung2thfc.mirror.aliyuncs.com/)
|
||||
|
||||
docker官方镜像仓库国内访问较慢,我们需要设置国内镜像服务:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /etc/docker
|
||||
sudo tee /etc/docker/daemon.json <<-'EOF'
|
||||
{"registry-mirrors": ["https://kaytushy.mirror.aliyuncs.com"]}
|
||||
{"registry-mirrors": ["https://registry.docker-cn.com"]}
|
||||
EOF
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart docker
|
||||
|
BIN
linux/ubuntu/assets/image-20230603140051442.png
Normal file
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 440 KiB |
21
linux/ubuntu/开机启动错误.md
Normal file
@ -0,0 +1,21 @@
|
||||
# **Linux Ubuntu开机遇见的 [Firmware Bug] initramfs 问题**
|
||||
|
||||
[Firmware Bug]: TSC_DEADLINE disabled due to Errata; please update microcode to [version](https://so.csdn.net/so/search?q=version&spm=1001.2101.3001.7020): 0x22 (or later)
|
||||
|
||||
BusyBox v1.27.2 ([Ubuntu](https://so.csdn.net/so/search?q=Debian&spm=1001.2101.3001.7020) 1:1.27.2-2) built-in shell (ash)
|
||||
|
||||
Enter 'help' for a list of built-in commands.
|
||||
|
||||
结果如下图:
|
||||
|
||||
![image-20230603140051442](assets/image-20230603140051442.png)
|
||||
|
||||
|
||||
|
||||
如果像我这样子是 /dev/mapper/ubuntu--vg-** 的错误,用 fsck 来处理
|
||||
|
||||
如我的这个情况,就输入:fsck -y /dev/mapper/ubuntu--vg-ubuntu--lv
|
||||
|
||||
输入后执行,执行完毕后输入:exit
|
||||
|
||||
就可以进入系统了。
|
BIN
数据结构/排序/assets/v2-0138cf1379dff2b5815b03e62b024e23_r.jpg
Normal file
After Width: | Height: | Size: 9.6 KiB |
BIN
数据结构/排序/assets/v2-0d3f6598d55a01b13ce919a500c2e57e_r.jpg
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
数据结构/排序/assets/v2-12309791bbace4b888cf76b60985bb78_r.jpg
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
数据结构/排序/assets/v2-1758c026cc6d45644b6526fe9ac5dd67_b.gif
Normal file
After Width: | Height: | Size: 444 KiB |
BIN
数据结构/排序/assets/v2-1c7e20f306ddc02eb4e3a50fa7817ff4_b.gif
Normal file
After Width: | Height: | Size: 614 KiB |
BIN
数据结构/排序/assets/v2-33a947c71ad62b254cab62e5364d2813_b.gif
Normal file
After Width: | Height: | Size: 333 KiB |
BIN
数据结构/排序/assets/v2-3dbbbd48d5d5f41ee8ad051a5e5dd5dd_r.jpg
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
数据结构/排序/assets/v2-4cc809678a4eb805d5dd3ce623b0888a_r.jpg
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
数据结构/排序/assets/v2-4fe88fdfbd70ed75aeac330e54a51dd8_b.jpg
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
数据结构/排序/assets/v2-67be619d3eb36596712f544f8c45f998_b.jpg
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
数据结构/排序/assets/v2-6b1b2484a37ab9e74fc29afed5668132_r.jpg
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
数据结构/排序/assets/v2-6e21d7a7597819b45f15971dd3148c28_r.jpg
Normal file
After Width: | Height: | Size: 8.7 KiB |
BIN
数据结构/排序/assets/v2-7f127d97b2d93319bb37d19159e3094e_b.gif
Normal file
After Width: | Height: | Size: 474 KiB |
BIN
数据结构/排序/assets/v2-8f800e1f7c6c2f8aad6e38e62634bf31_r.jpg
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
数据结构/排序/assets/v2-91b76e8e4dab9b0cad9a017d7dd431e2_b.gif
Normal file
After Width: | Height: | Size: 395 KiB |
BIN
数据结构/排序/assets/v2-9fedec8bf29186c6975cb13c53487809_r.jpg
Normal file
After Width: | Height: | Size: 8.5 KiB |
BIN
数据结构/排序/assets/v2-b135acdec9920d6f923d41bb791a60c2_b.jpg
Normal file
After Width: | Height: | Size: 7.0 KiB |
BIN
数据结构/排序/assets/v2-d8a129b5152a771a286979a7f339d18d_b.gif
Normal file
After Width: | Height: | Size: 614 KiB |
798
数据结构/排序/七大排序.md
Normal file
@ -0,0 +1,798 @@
|
||||
> 本文由 [简悦 SimpRead](http://ksria.com/simpread/) 转码, 原文地址 [zhuanlan.zhihu.com](https://zhuanlan.zhihu.com/p/580425726)
|
||||
|
||||
**概念:**
|
||||
|
||||
* 所谓排序,就是把一堆杂乱的数据,排成升序或降序 (递增 / 增减)。
|
||||
* **排序稳定性:** 假设一组数据 [1,2,9,5,5,6,8],进行升序排序后,两个 5 的相应位置不发生改变,即称为稳定的排序,否则就是不稳定排序。
|
||||
|
||||
![](assets/v2-6b1b2484a37ab9e74fc29afed5668132_r.jpg)
|
||||
|
||||
* **内部排序:** 数据元素全部放在内存中进行排序。
|
||||
* **外部排序:** 即将待排序的记录存储在外存中,排序时再把数据一部分一部分地调入内存进行排序,在排序过程中需要多次进行内存和外存之间地交换。
|
||||
|
||||
# 插入排序
|
||||
|
||||
-------
|
||||
|
||||
**思路:**
|
||||
|
||||
1. 默认为第一个元素自己是有序的,从第二个元素开始。
|
||||
2. 取出第二个元素 tmp,往前进行比较。
|
||||
3. 若该元素比 tmp 大,则将该元素往后移一位,直到找到比 tmp 小的。
|
||||
4. 找到比 tmp 小于等于的元素后,tmp 插入到该元素的下一位。
|
||||
5. 循环 2~4 步骤。
|
||||
|
||||
![](assets/v2-91b76e8e4dab9b0cad9a017d7dd431e2_b.gif)
|
||||
|
||||
步骤具体实现:
|
||||
|
||||
1. 定义下标 i ,遍历数组。默认 i 下标已经是有序的,把 i 下标元素存入 tmp。
|
||||
2. 定义 j,j 从 i-1 的位置,开始向前遍历,遇到比 tmp 大的元素,就把此时 j 下标的元素往后移一位,直到下标等于或小于 0 停止。
|
||||
3. j 下标元素小于 tmp,则把 tmp 元素插入该 j 下标的下一个位置。
|
||||
|
||||
**时间复杂度:** O(N^2);
|
||||
|
||||
**空间复杂度:** O(1);
|
||||
|
||||
**稳定性:** 稳定;
|
||||
|
||||
**具体代码实现:**
|
||||
|
||||
```java
|
||||
public static void insertSort(int[] array){
|
||||
//1.遍历数组
|
||||
for (int i = 0; i < array.length; i++) {
|
||||
int tmp = array[i];
|
||||
//2.往前遍历,进行插入
|
||||
int j = i-1;
|
||||
for ( ;j >=0 ; j--) {
|
||||
if(array[j] > tmp){
|
||||
int exchange = array[j];
|
||||
array[j+1] = array[j];
|
||||
array[j] = exchange;
|
||||
}else{
|
||||
//没有比tmp小的,退出循环
|
||||
break;
|
||||
}
|
||||
}
|
||||
//3.此时j下标元素比tmp小,tmp插入j下标的下一个位置
|
||||
array[j+1] = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**结论:**
|
||||
|
||||
1. 当数据趋于有序时,排序时间越快,最好的情况下时间复杂度为 O(N);
|
||||
2. 当把循环中 `array[j] > tmp`的大于号改为大于等于,此时就不是稳定的排序了。
|
||||
|
||||
# 希尔排序
|
||||
|
||||
-------
|
||||
|
||||
**思路:**
|
||||
|
||||
1. 先将待排序列进行预排序,使得待排序列接近有序,此时进行插入排序。
|
||||
2. 把待排序的数据分为多个组,每组间隔为 5 或 3…。
|
||||
3. 若此组的第一个元素大于最后一个元素,将此组第一个元素和最后一个元素交换。
|
||||
4. 重复上述操作,直到每组间隔只有 1 时,所有数据都在统一组内进行排好序。
|
||||
|
||||
![](assets/v2-d8a129b5152a771a286979a7f339d18d_b.gif)![](assets/v2-4cc809678a4eb805d5dd3ce623b0888a_r.jpg)
|
||||
|
||||
**步骤具体实现:**
|
||||
|
||||
1. 定义 gap = 数组长度 \ 2。
|
||||
2. 把待排序列分为 gap 个组,每个组的第一个元素和最后一个元素进行比较交换。
|
||||
3. 重复上述操作
|
||||
4. 当 gap 为 1 时,进行插入排序。
|
||||
|
||||
**时间复杂度:** O(N^1.3);
|
||||
|
||||
**空间复杂度:** O(1);
|
||||
|
||||
**稳定性:** 不稳定。
|
||||
|
||||
**具体代码实现:**
|
||||
|
||||
```java
|
||||
public static void shell(int[] array){
|
||||
int gap = array.length;
|
||||
while(gap > 1){
|
||||
gap /= 2;//分组
|
||||
//1.每组头和尾进行比较交换
|
||||
for (int i = 0; i < array.length; i++) {
|
||||
int tmp = array[i];
|
||||
//2.往前遍历,一次步长为gap
|
||||
int j = i-gap;
|
||||
for ( ;j >=0 ; j-=gap) {
|
||||
if(array[j] > tmp){
|
||||
int exchange = array[j];
|
||||
array[j+gap] = array[j];
|
||||
array[j] = exchange;
|
||||
}else{
|
||||
break;
|
||||
}
|
||||
}
|
||||
array[j+gap] = tmp;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**结论:**
|
||||
|
||||
1. 希尔排序是对插入排序的优化。
|
||||
2. 当 gap>1 时,都是预排序,目的是为了让数组更趋于有序,当 gap==1 时,数组已经接近有序,这样进行插入排序就会很快。
|
||||
3. 希尔排序的时间复杂度不容易计算,因为 gap 的取值方法很多,导致很难计算,因此我们按照 Knuth 提出的时间复杂度 O(N^1.3) 来算。
|
||||
|
||||
|
||||
# 选择排序
|
||||
|
||||
-------
|
||||
|
||||
**思路:**
|
||||
|
||||
每次从待排序列中选择一个最小值 (最大), 存放在序列的起始位置,直到全部待排序的数据排完。
|
||||
|
||||
![](assets/v2-1c7e20f306ddc02eb4e3a50fa7817ff4_b.gif)
|
||||
|
||||
**步骤具体实现:**
|
||||
|
||||
1. 定义`i`,假设待排序列`i`下标元素是最小值,用 min 记录当前`i`下标。
|
||||
2. 定义 j,从`i`下一个位置开始往后遍历,遇到小于`array[min]`时更新 min,min 指向每次遍历的最小值下标,直到遍历完一次数组。
|
||||
3. 一次遍历完后`array[i]`和 min 下标进行交换。
|
||||
4. 重复上述操作,直到待排序数据剩余 1 个元素。
|
||||
|
||||
**时间复杂度:** O(N^2);
|
||||
|
||||
**空间复杂度:** O(1);
|
||||
|
||||
**稳定性:** 不稳定;
|
||||
|
||||
**具体代码实现:**
|
||||
|
||||
```java
|
||||
public static void selectSort(int[] array){
|
||||
for (int i = 0; i < array.length; i++) {
|
||||
int min = i;
|
||||
for (int j = i+1; j < array.length; j++) {
|
||||
if(array[j] < array[min]){
|
||||
//更新min的值
|
||||
min = j;
|
||||
}
|
||||
}
|
||||
if(min != i){
|
||||
int tmp = array[i];
|
||||
array[i] = array[min];
|
||||
array[min] = tmp;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**结论:** 效率不是很好,实际中很少使用。
|
||||
|
||||
# 堆排序
|
||||
|
||||
------
|
||||
|
||||
**注意:排升序需要建大根堆,排降序建小根堆。** 此文章默认举例排升序。
|
||||
|
||||
**思路 & 具体步骤实现:**
|
||||
|
||||
1. 首先得建立一个大根堆。
|
||||
2. 把根节点与最后一个节点交换,每一次交换,最后一个节点向前走一步。
|
||||
3. 进行堆向下调整。
|
||||
|
||||
![](assets/v2-3dbbbd48d5d5f41ee8ad051a5e5dd5dd_r.jpg)
|
||||
|
||||
**时间复杂度:** O(N*logN);
|
||||
|
||||
**空间复杂度:** O(1);
|
||||
|
||||
**稳定性:** 不稳定;
|
||||
|
||||
**具体代码实现:**
|
||||
|
||||
```java
|
||||
public static void heapSort(int[] array){
|
||||
//0.创建大根堆
|
||||
createHeap(array);// 时间 O(N)
|
||||
int end = array.length-1;
|
||||
while (end > 0){
|
||||
//1.每次根和最后一个节点交换
|
||||
int tmp = array[0];
|
||||
array[0] = array[end];
|
||||
array[end] = tmp;
|
||||
//2.向下调整
|
||||
shiftDown(array,0,end);
|
||||
//3.最后一个节点已经是有升序的了
|
||||
end--;
|
||||
}
|
||||
}
|
||||
private static void createHeap(int[] array){
|
||||
//(array.length-1-1)->减一个1是数组下标是从0开始的,
|
||||
//减两个1是二叉树的概念:已知孩子节点求父亲节点 ->(i-1)/2
|
||||
for (int parent = (array.length-1-1)/2; parent >=0 ; parent--) {
|
||||
//向下调整
|
||||
shiftDown(array,parent,array.length);
|
||||
}
|
||||
}
|
||||
private static void shiftDown(int[] array,int parent,int len){
|
||||
int child = (parent*2)+1;
|
||||
while(child < len){
|
||||
if(child+1 < len && array[child+1] > array[child]){
|
||||
child++;
|
||||
}
|
||||
if(array[child] > array[parent]){
|
||||
int tmp = array[child];
|
||||
array[child] = array[parent];
|
||||
array[parent] = tmp;
|
||||
//继续向下调整
|
||||
parent = child;
|
||||
child = (parent*2)+1;
|
||||
}else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
# 冒泡排序
|
||||
|
||||
-------
|
||||
|
||||
**思路:** 根据序列中的两个记录键值比较结果来交换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的向前部移动。
|
||||
|
||||
![](assets/v2-33a947c71ad62b254cab62e5364d2813_b.gif)
|
||||
|
||||
**具体步骤实现:**
|
||||
|
||||
1. 定义`i`遍历数组,控制趟数,总体趟数比数组长度少 1;
|
||||
2. 每趟让一个较大值移动到尾部。
|
||||
3. 定义`j`每次从 0 下标进行两两比较交换。
|
||||
|
||||
**时间复杂度:** O(N^2);
|
||||
|
||||
**空间复杂度:** O(1);
|
||||
|
||||
**稳定性:** 稳定;
|
||||
|
||||
**具体代码实现:此处冒泡排序代码作了优化。**
|
||||
|
||||
```java
|
||||
public static void bubbleSort(int[] array){
|
||||
for (int i = 0; i < array.length-1; i++) {
|
||||
boolean flag = false;//判断此趟排序有没有交换
|
||||
for (int j = 0; j < array.length-1-i; j++) {
|
||||
if (array[j] > array[j + 1]) {
|
||||
int tmp = array[j];
|
||||
array[j] = array[j + 1];
|
||||
array[j + 1] = tmp;
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
//若flag一趟下来为false 说明数组已经有序
|
||||
if(flag == false){
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
# 快速排序
|
||||
|
||||
-------
|
||||
|
||||
**基本思想:任取待排序元素中的某元素作为基准值**,将待排序集合分割为两子序,左子序中所有元素均小于 **基准值**,右子序均大于 **基准值**,然后左右子序重复该过程,直到所有元素都排列在相应位置上。
|
||||
|
||||
## 6.1 Hoare 版
|
||||
|
||||
**思路:**
|
||||
|
||||
1. 取数组最左边或最右边为 key。
|
||||
2. 先从右边开始找到小于 key 值。
|
||||
3. 再从左边走找到大于 key 值,左右进行交换。
|
||||
4. 左右相遇后,相遇点为此次遍历的**基准值**
|
||||
5. 最后与 key 位置交换。
|
||||
|
||||
**具体实现步骤:**
|
||||
|
||||
1. 定义 key,指向数组最左边的元素。
|
||||
2. 定义 left 从数组左边开始遍历;定义 right 从右边开始遍历。
|
||||
3. 一定要 right 先走,再走 left。
|
||||
4. 当 right 和 left 相遇后,与 key 交换,相遇点为基准值。
|
||||
5. 遍历以该基准值分割的两子序。
|
||||
6. 递归重复上述操作。
|
||||
|
||||
**具体代码实现:**
|
||||
|
||||
```java
|
||||
public static void quickSort(int[] array){
|
||||
quickHelp(array,0,array.length-1);
|
||||
}
|
||||
|
||||
//具体实现排序调用函数
|
||||
private static void quickHelp(int[] array,int start,int end){
|
||||
if (start >= end){
|
||||
return;
|
||||
}
|
||||
//1.找基准值
|
||||
int pivot = hoare(array,start,end);
|
||||
//2.左右子序重复该操作
|
||||
quickHelp(array,start,pivot-1);
|
||||
quickHelp(array,pivot+1,end);
|
||||
}
|
||||
private static int hoare(int[] array,int left ,int right){
|
||||
|
||||
int index = left;//记录key下标
|
||||
int key = array[left];
|
||||
while(left < right){
|
||||
//1.先从右 找到比key小的值
|
||||
while(left < right && array[right] >= key){
|
||||
right--;
|
||||
}
|
||||
//2.再从左找到比key大的值
|
||||
while(left < right && array[left] <= key){
|
||||
left++;
|
||||
}
|
||||
//3.交换左右值
|
||||
int tmp = array[left];
|
||||
array[left] = array[right];
|
||||
array[right] = tmp;
|
||||
}
|
||||
//4.相遇点和key下标交换
|
||||
int tmp = array[left];
|
||||
array[left] = array[index];
|
||||
array[index] = tmp;
|
||||
return left;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**代码优化:**
|
||||
|
||||
此排序,递归下去就好像一颗二叉树,当排序的是有序时,就只有左子树或者只有右子树,此时排序的时间复杂度最高,所有:在找基准值先,尽量让左右子树划分平衡。当递归到一定层数是进行插入排序,因为越往下层遍历,这一层的递归次数越多,比如第一层递归 2 次,第二次递归 4 次…。
|
||||
|
||||
**步骤:**
|
||||
|
||||
1. 设定一个边界值,达到边界值时进行插入排序。
|
||||
2. 保证每次划分均匀:(3 个数找中数)
|
||||
如待排序:1 2 3 4 5 把 1 和 3 和 5 进行比较,找出中间值,把最左边值和中间值交换。
|
||||
|
||||
**具体代码实现:**
|
||||
|
||||
```java
|
||||
public static void quickSort(int[] array){
|
||||
quickHelp(array,0,array.length-1);
|
||||
}
|
||||
|
||||
//具体实现排序调用函数
|
||||
private static void quickHelp(int[] array,int start,int end){
|
||||
if (start >= end){
|
||||
return;
|
||||
}
|
||||
//对start - end 区间进行插入排序
|
||||
if(start <= 15){
|
||||
insertSortHelp(array,start,end);
|
||||
}
|
||||
//在找基准前尽量保证左右划分均匀
|
||||
int index = findMin(array,start,end);
|
||||
swap(array,start,index);
|
||||
|
||||
//1.找基准值
|
||||
int pivot = hoare(array,start,end);
|
||||
//2.左右子序重复该操作
|
||||
quickHelp(array,start,pivot-1);
|
||||
quickHelp(array,pivot+1,end);
|
||||
}
|
||||
public static void insertSortHelp(int[] array,int left,int right){
|
||||
for (int i = left; i <= right; i++) {
|
||||
int tmp = array[i];
|
||||
int j = i-1;
|
||||
for ( ;j >=0 ; j--) {
|
||||
if(array[j] > tmp){
|
||||
swap(array,j,j+1);
|
||||
}else{
|
||||
break;
|
||||
}
|
||||
}
|
||||
array[j+1] = tmp;
|
||||
}
|
||||
}
|
||||
private static int findMin(int[] array,int left,int right){
|
||||
int midIndex = (left+right)/2;
|
||||
|
||||
if(array[left] < array[right]){
|
||||
if(array[left] > array[midIndex]){
|
||||
return left;
|
||||
}else if(array[right] < array[midIndex]){
|
||||
return right;
|
||||
}else {
|
||||
return midIndex;
|
||||
}
|
||||
}else{//array[left] > array[right]
|
||||
if(array[left] < array[midIndex]){
|
||||
return left;
|
||||
}else if(array[right] > array[midIndex]){
|
||||
return right;
|
||||
}else {
|
||||
return midIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
private static int hoare(int[] array,int left ,int right){
|
||||
|
||||
int index = left;//记录key下标
|
||||
int key = array[left];
|
||||
while(left < right){
|
||||
//1.先从右 找到比key小的值
|
||||
while(left < right && array[right] >= key){
|
||||
right--;
|
||||
}
|
||||
//2.再从左找到比key大的值
|
||||
while(left < right && array[left] <= key){
|
||||
left++;
|
||||
}
|
||||
//3.交换左右值
|
||||
swap(array,left,right);
|
||||
}
|
||||
//4.相遇点和key下标交换
|
||||
swap(array,left,index);
|
||||
return left;
|
||||
}
|
||||
private static void swap(int[] array,int i,int j) {
|
||||
int tmp = array[i];
|
||||
array[i] = array[j];
|
||||
array[j] = tmp;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
6.2 挖坑法
|
||||
-------
|
||||
|
||||
**思路:**
|
||||
|
||||
大致思路根 Hoare 版本类似;
|
||||
|
||||
1. 选最左边的元素存入 key,在此位置形成一个坑位;
|
||||
2. 先从右开始遍历和 key 比较,再从左遍历,进行左右交换。
|
||||
3. 左右相交后,把 key 的放入相交点位置。
|
||||
|
||||
**具体实现步骤:**
|
||||
|
||||
1. 先将第一个元素存入临时变量 key,形成一个坑位。
|
||||
2. 定义 left 从数组左边开始走、right 从右边开始走。
|
||||
3. 先从右开始遍历找到比 key 小的元素,放入 left 的位置;
|
||||
4. 然后 left 找到比 key 大的元素,放入 right 的位置。
|
||||
5. 待 left 和 right 下标相遇后,把 key 的元素放入相交点。
|
||||
6. 重复上述操作。
|
||||
|
||||
![](assets/v2-7f127d97b2d93319bb37d19159e3094e_b.gif)
|
||||
|
||||
**具体代码实现【递归】:**
|
||||
|
||||
```java
|
||||
public static void digSort(int[] array,int start ,int end){
|
||||
if(start >= end){
|
||||
return;
|
||||
}
|
||||
int left = start;
|
||||
int right = end;
|
||||
int key = array[left];
|
||||
while(left < right){
|
||||
//1.先从右开始 找到比key小的
|
||||
while (left < right && array[right] >= key){
|
||||
right--;
|
||||
}
|
||||
//2.把小值填入左边的坑位
|
||||
array[left] = array[right];
|
||||
|
||||
//3.再从左开始 找到比key大的
|
||||
while (left < right && array[left] <= key){
|
||||
left++;
|
||||
}
|
||||
//4.把大值填入右边的坑位
|
||||
array[right] = array[left];
|
||||
}
|
||||
array[left] = key;
|
||||
//递归
|
||||
digSort(array,start,left-1);//遍历key左边
|
||||
digSort(array,left+1,end);//遍历key右边
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
6.3 前后指针法
|
||||
---------
|
||||
|
||||
了解即可,见得比较少,整体思路大体一样。
|
||||
|
||||
1. 定义 key 存储数组起始元素,prev 指向数组开始位置;cur 指 prev 后一个位置。
|
||||
2. 当 cur 下标元素小于 key,prev 向后走,并且此时 prev 下标的元素不等于 cur 下标元素,则进行交换
|
||||
3. 否则 cur 一直往后走,当 cur 走完时,prev 下标和数组最左边左边下标交换。
|
||||
4. 递归重复上述操作。
|
||||
|
||||
![](assets/v2-1758c026cc6d45644b6526fe9ac5dd67_b.gif)
|
||||
|
||||
```java
|
||||
public static void quick(int[] array,int start,int end){
|
||||
if(start >= end){
|
||||
return;
|
||||
}
|
||||
int pivot = partition(array,start,end);
|
||||
quick(array,start,pivot-1);
|
||||
quick(array,pivot+1,end);
|
||||
}
|
||||
private static int partition(int[] array, int left, int right) {
|
||||
int prev = left ;
|
||||
int cur = left+1;
|
||||
while (cur <= right) {
|
||||
if(array[cur] < array[left] && array[++prev] != array[left]) {
|
||||
swap(array,cur,prev);
|
||||
}
|
||||
cur++;
|
||||
}
|
||||
swap(array,prev,left);
|
||||
return prev;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
6.4 快速排序非递归
|
||||
-----------
|
||||
|
||||
**思路:使用栈,模拟递归**
|
||||
|
||||
1. 先找到基准
|
||||
2. 判断基准左右是否存在两个元素及以上。
|
||||
3. 把以基准为界左边的数组最左边下标和最右边下标入栈
|
||||
4. 再基准右边的数组最左边下标和最右边下标入栈
|
||||
5. 弹出栈顶两个下标,对此下标区间再进行找基准【弹出顺序:先右后左】
|
||||
6. 重复上述操作。
|
||||
7. 快速排序非递归最重要的就是找基准。
|
||||
|
||||
**具体代码实现:**
|
||||
|
||||
```java
|
||||
public static void quickSort2(int[] array){
|
||||
Stack<Integer> stack = new Stack<>();
|
||||
int left = 0;
|
||||
int right = array.length-1;
|
||||
//找基准
|
||||
int pivot = finMid(array,left,right);//排序核心代码
|
||||
//1.判断基准左边有没有2个元素 1 2 3 4 5 9
|
||||
if(pivot > left+1){ //若基准:3 left:1 pivot < left+1
|
||||
//把最左边和最右边的下标入栈
|
||||
stack.push(left);
|
||||
stack.push(pivot-1);
|
||||
}
|
||||
//2.判断基准右边有没有2个元素
|
||||
if(pivot < right-1){
|
||||
//把最左边和最右边的下标入栈
|
||||
stack.push(pivot+1);
|
||||
stack.push(right);
|
||||
}
|
||||
//3.弹出2个元素,重复上述操作
|
||||
while(!stack.isEmpty()){
|
||||
//注意弹出顺序:先右和左
|
||||
right = stack.pop();
|
||||
left = stack.pop();
|
||||
pivot = partition(array,left,right);
|
||||
|
||||
if(pivot > left+1){
|
||||
stack.push(left);
|
||||
stack.push(pivot-1);
|
||||
}
|
||||
if(pivot < right-1){
|
||||
stack.push(pivot+1);
|
||||
stack.push(right);
|
||||
}
|
||||
}
|
||||
}
|
||||
private static int finMid(int[] array,int left ,int right){
|
||||
|
||||
int index = left;//记录key下标
|
||||
int key = array[left];
|
||||
while(left < right){
|
||||
//1.先从右 找到比key小的值
|
||||
while(left < right && array[right] >= key){
|
||||
right--;
|
||||
}
|
||||
//2.再从左找到比key大的值
|
||||
while(left < right && array[left] <= key){
|
||||
left++;
|
||||
}
|
||||
//3.交换左右值
|
||||
swap(array,left,right);
|
||||
}
|
||||
//4.相遇点和key下标交换
|
||||
swap(array,left,index);
|
||||
return left;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**快速排序总结:**
|
||||
|
||||
1. 综合性能和使用场景比较好。
|
||||
2. 一般快速排序三种方法使用顺序:挖坑法 ->Hoare 法 -> 前后指针法
|
||||
|
||||
# 归并排序
|
||||
|
||||
-------
|
||||
|
||||
**核心思想:分而治之**
|
||||
|
||||
即:将待排序列拆分,再合并成为有序序列。
|
||||
|
||||
* **先把序列逐层进行拆分:**
|
||||
|
||||
![](assets/v2-0d3f6598d55a01b13ce919a500c2e57e_r.jpg)
|
||||
|
||||
* **当拆分到只有一个元素时,再从下往上逐层合并,首先对第一层序列号 1(元素 4),和序列号 2(元素 5) 进行合并**
|
||||
|
||||
1. 创建一个大数组,长度为序列号 1 和序列号 2 长度之和,s1、s2 指针分别指向两个小序列号 (数组) 的第一个元素,ret 指向大数组的第一个元素。
|
||||
|
||||
![](assets/v2-4fe88fdfbd70ed75aeac330e54a51dd8_b.jpg)
|
||||
|
||||
2. 比较 [s1]、[s2] 指向的元素,4<5,将 4 放入 ret 指向的空间,ret 往右走一步,s1 往右走一步。
|
||||
|
||||
![](assets/v2-67be619d3eb36596712f544f8c45f998_b.jpg)
|
||||
|
||||
3. 此时序列号 1(数组) 已经没有元素,直接将序列号 2 的元素放入大数组中。
|
||||
|
||||
![](assets/v2-b135acdec9920d6f923d41bb791a60c2_b.jpg)
|
||||
|
||||
4.[8]和 [1]、[7] 和[2]、[6]和[3],用同样方式进行合并。
|
||||
|
||||
![](assets/v2-12309791bbace4b888cf76b60985bb78_r.jpg)
|
||||
|
||||
5. 以 [4,5] 为序列 1,[1,8]为序列 2,继续进行合并。
|
||||
|
||||
![](assets/v2-8f800e1f7c6c2f8aad6e38e62634bf31_r.jpg)
|
||||
|
||||
6.[4]和 [1] 比较,4>1,把 [1] 放入 ret 指向的空间,[s2]往右走一步。
|
||||
|
||||
![](assets/v2-9fedec8bf29186c6975cb13c53487809_r.jpg)
|
||||
|
||||
7. 重复上述操作,直到把 [4,5] 和[1,8]合并成【1,4,5,8】。
|
||||
|
||||
以 [2,7] 为序列 3,[3,6]为序列 4,用同样的方式合并成为新的序列【2,3,6,7】;
|
||||
|
||||
![](assets/v2-6e21d7a7597819b45f15971dd3148c28_r.jpg)
|
||||
|
||||
8. 最后将 [1,4,5,8] 和[2,3,6,7]用同样的方式合并成新的序列
|
||||
|
||||
![](assets/v2-0138cf1379dff2b5815b03e62b024e23_r.jpg)
|
||||
|
||||
**时间复杂度:O(N*logN) :** 归并排序算法每次将序列折半分组,共需要 logN 轮。
|
||||
|
||||
**空间复杂度:O(N):** 归并排序算法排序过程中需要额外的一个序列去存储排序后的结果,所占空间是 N。
|
||||
|
||||
**稳定性:稳定**
|
||||
|
||||
**具体代码实现:**
|
||||
|
||||
```java
|
||||
public static void merge(int[] array,int start,int end){
|
||||
if(start >= end){
|
||||
return;
|
||||
}
|
||||
//1.分解
|
||||
int mid = (start+end)/2;//折半递归
|
||||
merge(array,start,mid);
|
||||
merge(array,mid+1,end);
|
||||
//2.合并 合并两个小数组为一个大数组
|
||||
mergeHelp(array,start,mid,end);
|
||||
}
|
||||
private static void mergeHelp(int[] array,int left,int mid,int right){
|
||||
//这里直接合并[4,5]和[1,8],因为[4]和[5]两个数组太小了,不好理解
|
||||
//第一个小数组下标范围
|
||||
int s1 = left;
|
||||
int e1 = mid;
|
||||
//第二个小数组下标范围
|
||||
int s2 = mid+1;
|
||||
int e2 = right;
|
||||
//合并的大数组
|
||||
int[] ret = new int[right-left+1];
|
||||
int k = 0;//ret下标
|
||||
while(s1 <= e1 && s2 <= e2){
|
||||
//进行比较
|
||||
if( array[s1] <= array[s2]){
|
||||
ret[k++] = array[s1++];
|
||||
} else{
|
||||
ret[k++] = array[s2++];
|
||||
}
|
||||
}
|
||||
//存放剩余的有序元素
|
||||
while(s1 <= e1){
|
||||
ret[k++] = array[s1++];
|
||||
}
|
||||
while(s2 <= e2){
|
||||
ret[k++] = array[s2++];
|
||||
}
|
||||
//把合并完的有序元素,放入原来的数组里->所有归并排序空间复杂度高
|
||||
for (int i = 0; i < k; i++) {
|
||||
//+left 因为每次合并ret存的元素对应的array下标在发生变化
|
||||
//若存入给ret的 5 6 7 8 对应原数组下标为 4 5 6 7
|
||||
//那+left(4),刚好存入原数组正确的位置
|
||||
array[i+left] = ret[i];
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**非递归版本:【模拟递归】**
|
||||
|
||||
```java
|
||||
public static void mergerSort(int[] array){
|
||||
int gap = 1;//模拟递归到每组序列只有一个元素
|
||||
while(gap < array.length){
|
||||
//合并
|
||||
for (int i = 0; i < array.length; i+= 2*gap) {
|
||||
int left = i;
|
||||
int mid = left+gap-1;
|
||||
//判断越界
|
||||
if(mid >= array.length){
|
||||
mid = array.length-1;
|
||||
}
|
||||
int right = mid+gap;
|
||||
//判断越界
|
||||
if(right >= array.length){
|
||||
right = array.length-1;
|
||||
}
|
||||
mergeHelp(array,left,mid,right);
|
||||
}
|
||||
gap *= 2;
|
||||
}
|
||||
}
|
||||
private static void mergeHelp(int[] array,int left,int mid,int right){
|
||||
//第一个小数组下标范围
|
||||
int s1 = left;
|
||||
int e1 = mid;
|
||||
//第二个小数组下标范围
|
||||
int s2 = mid+1;
|
||||
int e2 = right;
|
||||
//合并的大数组
|
||||
int[] ret = new int[right-left+1];
|
||||
int k = 0;//ret下标
|
||||
while(s1 <= e1 && s2 <= e2){
|
||||
//进行比较
|
||||
if( array[s1] <= array[s2]){
|
||||
ret[k++] = array[s1++];
|
||||
} else{
|
||||
ret[k++] = array[s2++];
|
||||
}
|
||||
}
|
||||
//存放剩余的有序元素
|
||||
while(s1 <= e1){
|
||||
ret[k++] = array[s1++];
|
||||
}
|
||||
while(s2 <= e2){
|
||||
ret[k++] = array[s2++];
|
||||
}
|
||||
for (int i = 0; i < k; i++) {
|
||||
array[i+left] = ret[i];
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**总结:**
|
||||
|
||||
1. 归并的缺点是需要 O(N) 的空间复杂度。
|
||||
2. 归并排序的思想更多是在解决磁盘中的外排序问题。
|
||||
|
||||
8. 排序算法复杂度及稳定性分析
|
||||
----------------
|
||||
|
||||
<table data-draft-node="block" data-draft-type="table" data-size="normal" data-row-style="normal"><tbody><tr><th>排序方法</th><th>最好</th><th>平均</th><th>最坏</th><th>空间复杂度</th><th>稳定性</th></tr><tr><td>冒泡排序</td><td>O(n)</td><td>O(n^2)</td><td>O(n^2)</td><td>O(1)</td><td>稳定</td></tr><tr><td>插入排序</td><td>O(n)</td><td>O(n^2)</td><td>O(n^2)</td><td>O(1)</td><td>稳定</td></tr><tr><td>选择排序</td><td>O(n^2)</td><td>O(n^2)</td><td>O(n^2)</td><td>O(1)</td><td>不稳定</td></tr><tr><td>希尔排序</td><td>O(n)</td><td>O(N^1.3)</td><td>O(n^2)</td><td>O(1)</td><td>不稳定</td></tr><tr><td>堆排序</td><td>O(n * log(n))</td><td>O(n * log(n))</td><td>O(n * log(n))</td><td>O(1)</td><td>不稳定</td></tr><tr><td>快速排序</td><td>O(n * log(n))</td><td>O(n * log(n))</td><td>O(n^2)</td><td>O(logn)~O(n)</td><td>不稳定</td></tr><tr><td>归并排序</td><td>O(n * log(n))</td><td>O(n * log(n))</td><td>O(n * log(n))</td><td>O(1)</td><td>稳定</td></tr></tbody></table>
|
BIN
计算机/数据结构/assets/v2-1c7e20f306ddc02eb4e3a50fa7817ff4_b.gif
Normal file
After Width: | Height: | Size: 614 KiB |
BIN
计算机/数据结构/assets/v2-2308bd4a8e9c27ac98cae39bf85270d1_b.jpg
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
计算机/数据结构/assets/v2-33a947c71ad62b254cab62e5364d2813_b.gif
Normal file
After Width: | Height: | Size: 333 KiB |
BIN
计算机/数据结构/assets/v2-3dbbbd48d5d5f41ee8ad051a5e5dd5dd_r.jpg
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
计算机/数据结构/assets/v2-4c61c5789fb31f6d88d6ae9511c29d01_r.jpg
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
计算机/数据结构/assets/v2-4cc809678a4eb805d5dd3ce623b0888a_r.jpg
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
计算机/数据结构/assets/v2-60ce4a3f1a64e355c7b7dd5ed4869469_r.jpg
Normal file
After Width: | Height: | Size: 63 KiB |
BIN
计算机/数据结构/assets/v2-6b1b2484a37ab9e74fc29afed5668132_r.jpg
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
计算机/数据结构/assets/v2-7cfc15c09cbdcfb561ce94818ced23b2_r.jpg
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
计算机/数据结构/assets/v2-91b76e8e4dab9b0cad9a017d7dd431e2_b.gif
Normal file
After Width: | Height: | Size: 395 KiB |
BIN
计算机/数据结构/assets/v2-9d4647230b6195e736a9c49675e9ef3c_r.jpg
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
计算机/数据结构/assets/v2-d8a129b5152a771a286979a7f339d18d_b.gif
Normal file
After Width: | Height: | Size: 614 KiB |
BIN
计算机/数据结构/assets/v2-f7503fa4e3323f0aa3b49c8fa8be8f00_r.jpg
Normal file
After Width: | Height: | Size: 120 KiB |
550
计算机/数据结构/数据结构原理及分析考点.md
Normal file
@ -0,0 +1,550 @@
|
||||
# 数据结构原理及分析考点
|
||||
|
||||
## 线性数据结构
|
||||
|
||||
### 数组
|
||||
|
||||
- 数组是一种线性数据结构,可以容纳固定数量的元素。
|
||||
|
||||
- 数组中的每个元素都有固定的索引,这是访问数组元素的一种有效方式。
|
||||
- 数组的大小在创建时确定,不能动态扩展或缩小。
|
||||
- 数组在内存中是连续的,这使得某些操作(如随机访问)非常快速。
|
||||
- 但是,如果需要动态调整大小,可能需要创建新的数组并复制数据,这是不高效的。
|
||||
|
||||
### 链表
|
||||
|
||||
- 链表是一种线性数据结构,可以动态添加或删除元素。
|
||||
- 每个元素包含数据和一个指向下一个元素的指针。
|
||||
- 链表不需要连续的内存空间,因此可以高效地进行插入和删除操作。
|
||||
- 但是,由于需要额外存储指针信息,因此链表的存储空间利用率可能较低。
|
||||
- 访问链表中的元素通常需要从头开始遍历,因此速度较慢。
|
||||
|
||||
#### (1)单链表
|
||||
|
||||
单链表单向链表只有一个方向,结点只有一个后继指针next指向后面的节点。我们习惯性地把第一个结点叫作头结点,链表通常有一个不保存任何值的head节点(头结点),通过头结点我们可以遍历整个链表。尾结点通常指向null。
|
||||
|
||||
#### (2)循环链表
|
||||
|
||||
循环链表其实是一种特殊的单链表,和单链表不同的是循环链表的尾结点不是指向null,而是指向链表的头结点。
|
||||
|
||||
#### (3)双向链表
|
||||
|
||||
双向链表包含两个指针,一个prev指向前一个节点,一个next指向后一个节点。
|
||||
|
||||
#### (4)双向循环链表
|
||||
|
||||
双向循环链表最后一个节点的next指向head,而head的prev指向最后一个节点,构成一个环。
|
||||
|
||||
### 队列
|
||||
|
||||
- 队列是一种先进先出(FIFO)的数据结构,通常用于管理需要按特定顺序处理的任务。
|
||||
- 队列中的元素只能从一端(队尾)添加,从另一端(队首)删除。
|
||||
- 队列的主要操作是入队(添加元素)和出队(删除元素),以及检查队列是否为空或已满。
|
||||
- 队列通常在内部维护一个双向链表实现其操作。
|
||||
|
||||
队列是先进先出的线性表。在具体应用中通常用链表或者数组来实现,用数组实现的队列叫作顺序队列,用链表实现的队列叫作链式队列。队列只允许在后端(rear)进行插入操作也就是入队enqueue,在前端(front)进行删除操作也就是出队dequeue。
|
||||
|
||||
### 栈
|
||||
|
||||
- 栈是一种后进先出(LIFO)的数据结构,通常用于跟踪正在进行的任务或操作的层次结构。
|
||||
- 栈中的元素只能从顶部添加和删除。
|
||||
- 栈的主要操作是push(添加元素)和pop(删除元素),以及检查栈是否为空或已满。
|
||||
- 栈也可以用数组或内部维护的链表来实现。
|
||||
|
||||
栈按照后进先出的原理运作。在栈中,push和pop的操作都发生在栈顶。
|
||||
用数组实现的栈叫作顺序栈,用链表实现的栈叫作链式栈。
|
||||
|
||||
## 非线性数据结构
|
||||
|
||||
### 二维数组
|
||||
|
||||
在一维数组中,每个元素也是一个数组元素,这样的数组称为二维数组。也可以理解为,二维数组是一个特殊的一维数组,这个一维数组的每个元素都是一维数组
|
||||
|
||||
### 多维数组
|
||||
|
||||
多维数组:多维数组是指超过二维的数组,通常在数学和计算机科学中用于表示高维空间数据和算法
|
||||
|
||||
### 二叉树
|
||||
|
||||
### 图
|
||||
|
||||
### 堆
|
||||
|
||||
堆(heap)就是用数组实现的二叉树,它没有使用父指针或者子指针。
|
||||
|
||||
堆根据” 堆属性 “来排序,该属性决定了树中节点的位置。
|
||||
|
||||
堆的应用:
|
||||
|
||||
- 构建优先队列
|
||||
- 堆排序
|
||||
- 快速找到集合中的极值
|
||||
|
||||
#### 什么是堆属性?
|
||||
|
||||
堆的定义是:一个数据集合 k={k0,k1,k2,...,kn},把它的所有元素按照完全二叉树的顺序储存方式存储在一个一维数组中,且满足下面的性质:
|
||||
|
||||
1. 每个节点的值**总是**不大于或者不小于其父节点的值
|
||||
2. 是一棵完全二叉树
|
||||
|
||||
有两种情况:
|
||||
|
||||
1. 父节点都不小于子节点的值,称为最大堆
|
||||
2. 父节点都不大于子节点的值,称为最小堆
|
||||
|
||||
注意,还需要是一棵完全二叉树,即除了最后一层节点的度不为 2,其余每个节点的度都是 2,如果最后一层的节点度不是 2,那么要求左满右不满。
|
||||
|
||||
下面举个例子,哪些是最大堆,哪些是最小堆。
|
||||
|
||||
![img](assets/v2-f7503fa4e3323f0aa3b49c8fa8be8f00_r.jpg)
|
||||
|
||||
也就是最大堆从上往下数值减小,最小堆从上往下是数值变大,但同一层的节点的数值没有固定顺序。
|
||||
|
||||
这个要与二叉搜索树区别开,二叉搜索树中,左节点必须小于父节点,右节点必须大于父节点。
|
||||
|
||||
根据最大堆和最小堆的属性,可以快速访问到最值。
|
||||
|
||||
#### 如何理解:用数组实现的树
|
||||
|
||||
在堆的定义中,说到” 所有元素按照完全二叉树的顺序储存方式存放在一个一维数组中 “,这里的顺序存储是什么意思?
|
||||
|
||||
顺序存储在物理上就是一个数组,但在逻辑上,我们理解为一颗二叉树,准确地说,应该是完全二叉树。
|
||||
|
||||
我们前面说到,堆可以表示为完全二叉树,这是相当节省空间的,为什么不表示为普通二叉树呢?
|
||||
|
||||
![img](assets/v2-60ce4a3f1a64e355c7b7dd5ed4869469_r.jpg)
|
||||
|
||||
上图说明,如果将数组表示为非完全二叉树,则顺序储存时会造成空间的浪费,有些地址没有值。这就是为什么堆需要完全二叉树。此外,完全二叉树表示为数组时,父节点总是在子节点前面。
|
||||
|
||||
所以,堆总是具有这样的形状:
|
||||
|
||||
![img](assets/v2-2308bd4a8e9c27ac98cae39bf85270d1_b.jpg)
|
||||
|
||||
在树中的每个节点都满足堆属性。
|
||||
|
||||
### 广义表
|
||||
|
||||
广义表也称列表(lists)是线性表的扩展,是一种可以嵌套的数据结构。广义表中的数据元素不仅有元素或者节点的名字,而且有元素或者节点的值,并且广义表可以共享,也就是说一个广义表可以被其他的广义表共享12。
|
||||
|
||||
广义表的定义和性质如下:
|
||||
|
||||
- 广义表的长度定义为最外层包含元素个数。
|
||||
- 广义表的深度定义为该广义表展开后所含括号的重数。其中原子的深度为0,空表的深度为1。
|
||||
- 广义表可以为其他表共享,被共享表的值可以不必列出,而是通过名称引用。
|
||||
- 广义表可以是一个递归的表。递归表的深度是无穷的值,长度是有限值。
|
||||
|
||||
广义表(Lists,又称列表)是线性表的推广。广义表是n(n≥0)个元素a1,a2,a3,…,an的有限序列,其中ai或者是原子项,或者是一个广义表。若广义表LS(n>=1)非空,则a1是LS的表头,其余元素组成的表(a2,…an)称为LS的表尾。广义表的元素可以是广义表,也可以是原子,广义表的元素也可以为空。表尾是指除去表头后剩下的元素组成的表,表头可以为表或单元素值。所以表尾不可以是单个元素值。
|
||||
|
||||
#### 例子:
|
||||
|
||||
A=()——A是一个空表,其长度为零。
|
||||
|
||||
B=(e)——表B只有一个原子e,B的长度为1。
|
||||
|
||||
C=(a,(b,c,d))——表C的长度为2,两个元素分别为原子a和子表(b,c,d)。
|
||||
|
||||
D=(A,B,C)——表D的长度为3,三个元素都是广义 表。显然,将子表的值代入后,则有D=(( ),(e),(a,(b,c,d)))。
|
||||
|
||||
E=(a,E)——这是一个递归的表,它的长度为2,E相当于一个无限的广义表E=(a,(a,(a,(a,…)))).
|
||||
|
||||
#### 三个结论:
|
||||
|
||||
1.广义表的元素可以是子表,而子表的元素还可以是子表。由此,广义表是一个多层次的结构,可以用图形象地表示
|
||||
|
||||
2.广义表可为其它表所共享。例如在上述例4中,广义表A,B,C为D的子表,则在D中可以不必列出子表的值,而是通过子表的名称来引用。
|
||||
|
||||
3.广义表的递归性
|
||||
|
||||
#### 考点:
|
||||
|
||||
1.广义表是0个或多个单因素或子表组成的有限序列,广义表可以是自身的子表,广义表的长度n>=0,所以可以为空表。广义表的同级元素(直属于同一个表中的各元素)具有线性关系
|
||||
|
||||
2.广义表的表头为空,并不代表该广义表为空表。广义表()和(())不同。前者是长度为0的空表,对其不能做求表头和表尾的运算;而后者是长度为l的非空表(只不过该表中惟一的一个元素是空表),对其可进行分解,得到的表头和表尾均是空表()
|
||||
|
||||
3.已知广义表LS=((a,b,c),(d,e,f)),运用head和tail函数取出LS中原子e的运算是head(tail(head(tail(LS)))。根据表头、表尾的定义可知:任何一个非空广义表的表头是表中第一个元素,它可以是原子,也可以是子表,而其表尾必定是子表。也就是说,广义表的head操作,取出的元素是什么,那么结果就是什么。但是tail操作取出的元素外必须加一个表——“()“。tail(LS)=((d,e,f));head(tail(LS))=(d,e,f);tail(head(tail(LS)))=(e,f);head(tail(head(tail(LS))))=e。
|
||||
|
||||
4.二维以上的数组其实是一种特殊的广义表
|
||||
|
||||
5.在(非空)广义表中:1、表头head可以是原子或者一个表 2、表尾tail一定是一个表 3.广义表难以用顺序存储结构 4.广义表可以是一个多层次的结构
|
||||
|
||||
## 排序
|
||||
|
||||
**概念:**
|
||||
|
||||
* 所谓排序,就是把一堆杂乱的数据,排成升序或降序 (递增 / 增减)。
|
||||
* **排序稳定性:** 假设一组数据 [1,2,9,5,5,6,8],进行升序排序后,两个 5 的相应位置不发生改变,即称为稳定的排序,否则就是不稳定排序。
|
||||
|
||||
![](assets/v2-6b1b2484a37ab9e74fc29afed5668132_r.jpg)
|
||||
|
||||
* **内部排序:** 数据元素全部放在内存中进行排序。
|
||||
* **外部排序:** 即将待排序的记录存储在外存中,排序时再把数据一部分一部分地调入内存进行排序,在排序过程中需要多次进行内存和外存之间地交换。
|
||||
|
||||
### 插入排序
|
||||
|
||||
-------
|
||||
|
||||
**思路:**
|
||||
|
||||
1. 默认为第一个元素自己是有序的,从第二个元素开始。
|
||||
2. 取出第二个元素 tmp,往前进行比较。
|
||||
3. 若该元素比 tmp 大,则将该元素往后移一位,直到找到比 tmp 小的。
|
||||
4. 找到比 tmp 小于等于的元素后,tmp 插入到该元素的下一位。
|
||||
5. 循环 2~4 步骤。
|
||||
|
||||
![](assets/v2-91b76e8e4dab9b0cad9a017d7dd431e2_b.gif)
|
||||
|
||||
步骤具体实现:
|
||||
|
||||
1. 定义下标 i ,遍历数组。默认 i 下标已经是有序的,把 i 下标元素存入 tmp。
|
||||
2. 定义 j,j 从 i-1 的位置,开始向前遍历,遇到比 tmp 大的元素,就把此时 j 下标的元素往后移一位,直到下标等于或小于 0 停止。
|
||||
3. j 下标元素小于 tmp,则把 tmp 元素插入该 j 下标的下一个位置。
|
||||
|
||||
**时间复杂度:** O(N^2);
|
||||
|
||||
**空间复杂度:** O(1);
|
||||
|
||||
**稳定性:** 稳定;
|
||||
|
||||
**具体代码实现:**
|
||||
|
||||
```java
|
||||
public static void insertSort(int[] array){
|
||||
//1.遍历数组
|
||||
for (int i = 0; i < array.length; i++) {
|
||||
int tmp = array[i];
|
||||
//2.往前遍历,进行插入
|
||||
int j = i-1;
|
||||
for ( ;j >=0 ; j--) {
|
||||
if(array[j] > tmp){
|
||||
int exchange = array[j];
|
||||
array[j+1] = array[j];
|
||||
array[j] = exchange;
|
||||
}else{
|
||||
//没有比tmp小的,退出循环
|
||||
break;
|
||||
}
|
||||
}
|
||||
//3.此时j下标元素比tmp小,tmp插入j下标的下一个位置
|
||||
array[j+1] = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**结论:**
|
||||
|
||||
1. 当数据趋于有序时,排序时间越快,最好的情况下时间复杂度为 O(N);
|
||||
2. 当把循环中 `array[j] > tmp`的大于号改为大于等于,此时就不是稳定的排序了。
|
||||
|
||||
### 希尔排序
|
||||
|
||||
-------
|
||||
|
||||
**思路:**
|
||||
|
||||
1. 先将待排序列进行预排序,使得待排序列接近有序,此时进行插入排序。
|
||||
2. 把待排序的数据分为多个组,每组间隔为 5 或 3…。
|
||||
3. 若此组的第一个元素大于最后一个元素,将此组第一个元素和最后一个元素交换。
|
||||
4. 重复上述操作,直到每组间隔只有 1 时,所有数据都在统一组内进行排好序。
|
||||
|
||||
![](assets/v2-d8a129b5152a771a286979a7f339d18d_b.gif)![](assets/v2-4cc809678a4eb805d5dd3ce623b0888a_r.jpg)
|
||||
|
||||
**步骤具体实现:**
|
||||
|
||||
1. 定义 gap = 数组长度 \ 2。
|
||||
2. 把待排序列分为 gap 个组,每个组的第一个元素和最后一个元素进行比较交换。
|
||||
3. 重复上述操作
|
||||
4. 当 gap 为 1 时,进行插入排序。
|
||||
|
||||
**时间复杂度:** O(N^1.3);
|
||||
|
||||
**空间复杂度:** O(1);
|
||||
|
||||
**稳定性:** 不稳定。
|
||||
|
||||
**具体代码实现:**
|
||||
|
||||
```java
|
||||
public static void shell(int[] array){
|
||||
int gap = array.length;
|
||||
while(gap > 1){
|
||||
gap /= 2;//分组
|
||||
//1.每组头和尾进行比较交换
|
||||
for (int i = 0; i < array.length; i++) {
|
||||
int tmp = array[i];
|
||||
//2.往前遍历,一次步长为gap
|
||||
int j = i-gap;
|
||||
for ( ;j >=0 ; j-=gap) {
|
||||
if(array[j] > tmp){
|
||||
int exchange = array[j];
|
||||
array[j+gap] = array[j];
|
||||
array[j] = exchange;
|
||||
}else{
|
||||
break;
|
||||
}
|
||||
}
|
||||
array[j+gap] = tmp;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**结论:**
|
||||
|
||||
1. 希尔排序是对插入排序的优化。
|
||||
2. 当 gap>1 时,都是预排序,目的是为了让数组更趋于有序,当 gap==1 时,数组已经接近有序,这样进行插入排序就会很快。
|
||||
3. 希尔排序的时间复杂度不容易计算,因为 gap 的取值方法很多,导致很难计算,因此我们按照 Knuth 提出的时间复杂度 O(N^1.3) 来算。
|
||||
|
||||
### 选择排序
|
||||
|
||||
-------
|
||||
|
||||
**思路:**
|
||||
|
||||
每次从待排序列中选择一个最小值 (最大), 存放在序列的起始位置,直到全部待排序的数据排完。
|
||||
|
||||
![](assets/v2-1c7e20f306ddc02eb4e3a50fa7817ff4_b.gif)
|
||||
|
||||
**步骤具体实现:**
|
||||
|
||||
1. 定义`i`,假设待排序列`i`下标元素是最小值,用 min 记录当前`i`下标。
|
||||
2. 定义 j,从`i`下一个位置开始往后遍历,遇到小于`array[min]`时更新 min,min 指向每次遍历的最小值下标,直到遍历完一次数组。
|
||||
3. 一次遍历完后`array[i]`和 min 下标进行交换。
|
||||
4. 重复上述操作,直到待排序数据剩余 1 个元素。
|
||||
|
||||
**时间复杂度:** O(N^2);
|
||||
|
||||
**空间复杂度:** O(1);
|
||||
|
||||
**稳定性:** 不稳定;
|
||||
|
||||
**具体代码实现:**
|
||||
|
||||
```java
|
||||
public static void selectSort(int[] array){
|
||||
for (int i = 0; i < array.length; i++) {
|
||||
int min = i;
|
||||
for (int j = i+1; j < array.length; j++) {
|
||||
if(array[j] < array[min]){
|
||||
//更新min的值
|
||||
min = j;
|
||||
}
|
||||
}
|
||||
if(min != i){
|
||||
int tmp = array[i];
|
||||
array[i] = array[min];
|
||||
array[min] = tmp;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**结论:** 效率不是很好,实际中很少使用。
|
||||
|
||||
### 冒泡排序
|
||||
|
||||
-------
|
||||
|
||||
**思路:** 根据序列中的两个记录键值比较结果来交换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的向前部移动。
|
||||
|
||||
![](assets/v2-33a947c71ad62b254cab62e5364d2813_b.gif)
|
||||
|
||||
**具体步骤实现:**
|
||||
|
||||
1. 定义`i`遍历数组,控制趟数,总体趟数比数组长度少 1;
|
||||
2. 每趟让一个较大值移动到尾部。
|
||||
3. 定义`j`每次从 0 下标进行两两比较交换。
|
||||
|
||||
**时间复杂度:** O(N^2);
|
||||
|
||||
**空间复杂度:** O(1);
|
||||
|
||||
**稳定性:** 稳定;
|
||||
|
||||
**具体代码实现:此处冒泡排序代码作了优化。**
|
||||
|
||||
```java
|
||||
public static void bubbleSort(int[] array){
|
||||
for (int i = 0; i < array.length-1; i++) {
|
||||
boolean flag = false;//判断此趟排序有没有交换
|
||||
for (int j = 0; j < array.length-1-i; j++) {
|
||||
if (array[j] > array[j + 1]) {
|
||||
int tmp = array[j];
|
||||
array[j] = array[j + 1];
|
||||
array[j + 1] = tmp;
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
//若flag一趟下来为false 说明数组已经有序
|
||||
if(flag == false){
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### 快速排序
|
||||
|
||||
## 查找算法
|
||||
|
||||
### 线性查找
|
||||
|
||||
#### 定义
|
||||
|
||||
线性查找「 Linear Search」是一种最基础的查找方法,其从数据结构的一端开始,依次访问每个元素,直到另一端后停止。线性查找实质上就是遍历数据结构 + 判断条件。比如,我们想要在数组`nums`中查找目标元素`target`的对应索引,那么可以在数组中进行线性查找。
|
||||
|
||||
![](assets/v2-9d4647230b6195e736a9c49675e9ef3c_r.jpg)
|
||||
|
||||
#### 代码实现
|
||||
|
||||
```java
|
||||
private static int sequenceSearch(int[] array,int target){
|
||||
for(int i=0;i<array.length;i++){
|
||||
if(target==array[i])
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
#### 复杂度分析
|
||||
|
||||
**时间复杂度** **O(n)**:其中 n 为数组或链表长度。
|
||||
|
||||
**空间复杂度** **O(1)**:无需使用额外空间。
|
||||
|
||||
### 二分查找
|
||||
|
||||
#### 定义
|
||||
|
||||
二分查找 「Binary Search」利用数据的有序性,通过每轮缩小一半搜索区间来查找目标元素。
|
||||
|
||||
使用二分查找有两个前置条件:
|
||||
|
||||
* **要求输入数据是有序的**,这样才能通过判断大小关系来排除一半的搜索区间;
|
||||
* **二分查找仅适用于数组**,而在链表中使用效率很低,因为其在循环中需要跳跃式(非连续地)访问元素。
|
||||
|
||||
#### 代码实现
|
||||
|
||||
![](assets/v2-7cfc15c09cbdcfb561ce94818ced23b2_r.jpg)
|
||||
|
||||
```java
|
||||
static int binarySearch1(int arr[],int len,int target){
|
||||
/*初始化左右搜索边界*/
|
||||
int left=0,right=len-1;
|
||||
int mid;
|
||||
while(left<=right){
|
||||
/*中间位置:两边界元素之和/2向下取整*/
|
||||
mid=(left+right)/2;
|
||||
/*arr[mid]大于target,即要寻找的元素在左半边,所以需要设定右边界为mid-1,搜索左半边*/
|
||||
if(target<arr[mid]){
|
||||
right=mid-1;
|
||||
/*arr[mid]小于target,即要寻找的元素在右半边,所以需要设定左边界为mid+1,搜索右半边*/
|
||||
}else if(target>arr[mid]){
|
||||
left=mid+1;
|
||||
/*搜索到对应元素*/
|
||||
}else if(target==arr[mid]){
|
||||
return mid;
|
||||
}
|
||||
}
|
||||
/*搜索不到返回-1*/
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
#### 复杂度分析
|
||||
|
||||
**时间复杂度** **O(logn)**:其中 n 为数组或链表长度;每轮排除一半的区间,因此循环轮数为 logn,使用 O(logn) 时间。
|
||||
|
||||
**空间复杂度** **O(1)**:指针`i`,`j`使用常数大小空间。
|
||||
|
||||
### 哈希查找
|
||||
|
||||
#### 定义
|
||||
|
||||
「哈希查找 Hash Searching」借助一个哈希表来存储需要的「键值对 Key Value Pair」,我们可以在 O(1)时间下实现 “键→值” 映射查找,体现着 “以空间换时间” 的算法思想。
|
||||
|
||||
![](assets/v2-4c61c5789fb31f6d88d6ae9511c29d01_r.jpg)
|
||||
|
||||
#### 代码实现
|
||||
|
||||
```java
|
||||
public class HashSearch {
|
||||
|
||||
/*待查找序列*/
|
||||
static int[] array = {13, 29, 27, 28, 26, 30, 38};
|
||||
/* 初始化哈希表长度,此处哈希表容量设置的和array长度一样。
|
||||
* 其实正常情况下,哈希表长度应该要长于array长度,因为使用
|
||||
* 开放地址法时,可能会多使用一些空位置
|
||||
*/
|
||||
static int hashLength = 7;
|
||||
static int[] hashTable = new int[hashLength];
|
||||
|
||||
public static void main(String[] args) {
|
||||
/*将元素插入到哈希表中*/
|
||||
for (int i = 0; i < array.length; i++) {
|
||||
insertHashTable(hashTable, array[i]);
|
||||
}
|
||||
System.out.println("哈希表中的数据:");
|
||||
printHashTable(hashTable);
|
||||
|
||||
int data = 28;
|
||||
System.out.println("\n要查找的数据"+data);
|
||||
int result = searchHashTable(hashTable, data);
|
||||
if (result == -1) {
|
||||
System.out.println("对不起,没有找到!");
|
||||
} else {
|
||||
System.out.println("在哈希表中的位置是:" + result);
|
||||
}
|
||||
}
|
||||
|
||||
/*将元素插入到哈希表中*/
|
||||
public static void insertHashTable(int[] hashTable, int target) {
|
||||
int hashAddress = hash(hashTable, target);
|
||||
|
||||
/*如果不为0,则说明发生冲突*/
|
||||
while (hashTable[hashAddress] != 0) {
|
||||
/*利用开放定址法解决冲突,即向后寻找新地址*/
|
||||
hashAddress = (++hashAddress) % hashTable.length;
|
||||
}
|
||||
|
||||
/*将元素插入到哈希表中*/
|
||||
hashTable[hashAddress] = target;
|
||||
}
|
||||
|
||||
public static int searchHashTable(int[] hashTable, int target) {
|
||||
int hashAddress = hash(hashTable, target);
|
||||
|
||||
while (hashTable[hashAddress] != target) {
|
||||
/*寻找原始地址后面的位置*/
|
||||
hashAddress = (++hashAddress) % hashTable.length;
|
||||
/*查找到开放单元(未存放元素的位置)或 循环回到原点,表示查找失败*/
|
||||
if (hashTable[hashAddress] == 0 || hashAddress == hash(hashTable, target)) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return hashAddress;
|
||||
}
|
||||
|
||||
/*用除留余数法计算要插入元素的地址*/
|
||||
public static int hash(int[] hashTable, int data) {
|
||||
return data % hashTable.length;
|
||||
}
|
||||
|
||||
public static void printHashTable(int[] hashTable) {
|
||||
for(int i=0;i<hashTable.length;i++)
|
||||
System.out.print(hashTable[i]+" ");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 复杂度分析
|
||||
|
||||
**时间复杂度** **O(1)**:哈希表的查找操作使用 O(1) 时间。
|
||||
|
||||
**空间复杂度** **O(n)**:其中 n 为数组或链表长度。
|