doc 新增数据结构分析课常考知识点文档

This commit is contained in:
dashan 2023-10-09 23:12:04 +08:00
parent d255419f3b
commit 3a91dd5fd9
46 changed files with 1479 additions and 1 deletions

10
.idea/.gitignore vendored Normal file
View 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/

View 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>

View 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
View 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>

View File

@ -0,0 +1,3 @@
l
<web/docker-compose一键部署蘑菇博客设置SSL证书.md,0\f\0fd96cadf695f65b8aeed80166a160b5319a252d

6
.idea/vcs.xml Normal file
View 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
View 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
```

View File

@ -0,0 +1,2 @@
LTAI5tKPzZeKE6WAe2kM2pJE
aSmNeWpisOtGJAh1nvS05zUaDIxecr

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View 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
就可以进入系统了。

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 KiB

View 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. 定义 jj 从 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]`时更新 minmin 指向每次遍历的最小值下标,直到遍历完一次数组。
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 下标元素小于 keyprev 向后走,并且此时 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

View 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={k0k1k2...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或者是原子项或者是一个广义表。若广义表LSn>=1)非空则a1是LS的表头其余元素组成的表(a2,…an)称为LS的表尾。广义表的元素可以是广义表也可以是原子广义表的元素也可以为空。表尾是指除去表头后剩下的元素组成的表表头可以为表或单元素值。所以表尾不可以是单个元素值。
#### 例子:
A=——A是一个空表其长度为零。
B=e——表B只有一个原子eB的长度为1。
C=a,(b,c,d))——表C的长度为2两个元素分别为原子a和子表(b,c,d)。
D=ABC——表D的长度为3三个元素都是广义 表。显然将子表的值代入后则有D=(( ),(e),(a,(b,c,d)))。
E=a,E——这是一个递归的表它的长度为2E相当于一个无限的广义表E=(a,(a,(a,(a,…)))).
#### 三个结论:
1.广义表的元素可以是子表,而子表的元素还可以是子表。由此,广义表是一个多层次的结构,可以用图形象地表示
2.广义表可为其它表所共享。例如在上述例4中广义表ABC为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. 定义 jj 从 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]`时更新 minmin 指向每次遍历的最小值下标,直到遍历完一次数组。
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 为数组或链表长度。