动力节点文档:📎数据结构和算法2024.pdf
一.数据结构和算法概述
1.1算法
是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制.也就是说,能够对一定规范的输入,在有限时间内获得所要求的输出.如果一个算法有缺陷,后不适合于某个问题,执行这个算法将不会解决这个问题.不同的算法可能用不同时间,空间或效率来完成同样的任务.一个算法的优劣可以用空间复杂度与时间复杂度来衡量.
算法是独立存在的一种解决问题的方法和思想.
对于算法而言,实现的语言并不重要,重要的是思想.
1.2数据结构及分类
数据结构是把数据组织起来,为了更方便地使用数据我们为了解决问题,需要将数据保存下来,然后根据数据发存储方式来设计算法实现进行处理,那么数据的存储方式不同就会导致需要不同的算法进行处理.我们希望算法解决问题的效率越快越好,于是我们就需要考虑数据究竟如何保存的问题,这就是数据结构.
数据结构是计算机存储,组织数据的方式,数据结构是指相互之间存在一种或多种特定关系的数据元素的集合.
1.2.1逻辑结构
逻辑结构式从具体问题中抽象出来的模型,是抽象意义上的结构,按照对象中数据元素之间的相互关系分类,也是我们后面课题中需要关注和讨论的问题.
a.集合结构:
集合结构中数据元素除了属于同一个集合外,他们之间没有任何其他的关系.
b.线性结构:
线性结构中的数据元素之间存在一对一的关系.
c.树形结构:
树形结构中的数据元素之间存在一对多的层次关系.
d.图形结构:
图形结构的数据元素是多对多的关系
1.2.2物理结构
逻辑结构在计算机中真正的表示方式(又称为映像)称为物理结构.也可以叫做存储结构.常见的物理结构有顺序存储结构和链式存储结构.
顺序存储结构:
把数据元素放在地址连续的存储单元里面,其数据间的逻辑关系和物理关系是一致的,比如我们常用的数组就是顺序存储结构.
顺序存储结构存在一定的弊端,需要插入或删除就需要对原储存结构进行重新排列
链式存储结构:
是把数据元素存放在任意的存储单元里面,这组存储单元可以是连续的也可以是不连续的.此时,数据元素之间并不能反映元素的逻辑关系.因此在链式存储结构中引进了一个指针存放数据元素的地址,这样通过地址就可以找到相关联数据元素的位置.
1.2.1线性结构和非线性结构
线性结构
- 线性结构作为最常用的数据结构,其特点是数据元素之间存在一对一的线性关系。
- 线性结构有两种不同的存储结构, 即顺序存储结构(数组)和链式存储结构(链表)。 顺序存储的线性表称为顺序表, 顺序表中的存储元素是连续的。
- 链式存储的线性表称为链表, 链表中的存储元素不一定是连续的, 元素结点存放 数据元素以及相邻元素的地址信息。
- 线性结构常见的有: 数组、 队列、 链表和栈。
非线性结构
二维数组,多维数组,广义表,树结构,图结构.
二.时间和空间复杂度
2.1算法的时间复杂度分析
我们要计算算法时间耗费情况, 首先我们得度量算法的执行时间, 那么如何度量呢 ? 郭后分析估算方法 : 比较容易想到的方法就是我们把算法执行若干次,然后拿个计时器在旁边计时,这种事后统计 的方法看上去的确不错,并且也并非要我们真的拿个计算器在旁边计算,因为计算机都提供了 计时的功能。 这种统计方法主要是通过设计好的测试程序和测试数据 , 利用计算机计时器对不 同的算法编制的程序的运行时间进行比较,从而确定算法效率的高低,但是这种方法有很大的 缺陷 : 必须依据算法实现编制好的测试程序, 通常要花费大量时间和精力, 测试完了如果发现 测试的是非常糟糕的算法, 那么之前所做的事情就全部白 费了 , 并且不同的测试环境 ( 硬件环境 ) 的差别导致测试的结果差异也很大。
long start = System.currentTimeMillis();
long end = System.currentTimeMillis();
System.out.println(end - start);
事前分析估算方法 : 在计算机程序编写前,依据统计方法对算法进行估算,经过总结,我们发现一个高级语言编写 的程序程序在计算机上运行所消耗的时间取决于下列因素 :
- 算法采用的策略和方案 ;
- 编译产生的代码质量 ;
- 问题的输入规模 ( 所谓的问题输入规模就是输入量的多少 );
- 机器执行指令的速度 ;
由此可见, 抛开这些与计算机硬件、软件有关的因素,一个程序的运行时间依赖于算法的好坏 和问题的输入规模。 如果算法固定, 那么该算法的执行时间就只和问题的输入规模有关系了。
上面这个例子中, 如果我们要精确的研究循环的条件执行了多少次,是一件很麻烦的事情,并 且, 由于真正计算和代码内循环的循环体, 所以, 在研究算法的效率时, 我们只考虑核心代码 的执行次数, 这样可以简化分析。 我们研究算法复杂度, 侧重的是当输入规模不断增大时, 算法的增长量的一个抽象 ( 规律 ) , 而不是精确地定位需要执行多少次, 因为如果是这样的话, 我们又得考虑回编译期优化等问题, 容易主次跌倒。 我们不关心编写程序所用的语言是什么,也不关心这些程序将跑在什么样的计算机上,我们只 关心它所实现的算法。这样, 不计那些循环索引的递增和循环终止的条件、变量声明、打印结 果等操作,最终在分析程序的运行时间时,最重要的是把程序看做是独立于程序设计语言的算 法或一系列步骤。我们分析一个算法的运行时间,最重要的就是把核心操作的次数和输入规模 关联起来。
n输入规模 f(n)执行次数
在我们比较算法随着输入规模的增长量时,可以有以下规则:
1.算法函数中的常数可以忽略;
2.算法函数中最高次幂的常数因子可以忽略;
3.算法函数中最高次幂越小,算法效率越高。
2.2时间频度
时间频度:一个算法花费的时间与算法中语句的执行次数成正比,哪一个算法中语句执行次数多,那么他所花费的时间越多. 一个算法语句执行次数称之为语句频度或时间
忽略常数项,忽略低次项,忽略系数.
2.3算法时间复杂度
2.3.1大O记法
用大写O()来体现算法时间复杂度的记法,称为大O记法.
如果忽略判断条件的执行次数和输出语句的执行次数,那么当输入规模为 n 时,以上算法执行
的次数分别为:
算法一:3 次
算法二:n+3 次
算法三:n^2+2 次
如果用大 O 记法表示上述每个算法的时间复杂度,应该如何表示呢?基于我们对函数渐近增长
的分析,推导大 O 阶的表示法有以下几个规则可以使用:
1.用常数 1 取代运行时间中的所有加法常数;
2.在修改后的运行次数中,只保留高阶项;
3.如果最高阶项存在,且常数因子不为 1,则去除与这个项相乘的常数;
所以,上述算法的大 O 记法分别为:
算法一: O(1)
算法二: O(n)
算法三: O(n^2)
2.3.2 常见的大 O 阶
2.3.2.1 线性阶:
一般含有非嵌套循环涉及线性阶,线性阶就是随着输入规模的扩大,对应计算次数呈直线增长,
例如:
上面这段代码,它的循环的时间复杂度为 O(n),因为循环体中的代码需要执行 n 次
2.3.2.2 平方阶
一般嵌套循环属于这种时间复杂度
上面这段代码,n=100,也就是说,外层循环每执行一次,内层循环就执行 100 次,那总共程
序想要从这两个循环中出来,就需要执行 100*100 次,也就是 n 的平方次,所以这段代码的时
间复杂度是 O(n^2)
2.3.2.3 立方阶
一般三层嵌套循环属于这种时间复杂度
上面这段代码,n=100,也就是说,外层循环每执行一次,中间循环循环就执行 100 次,中间
循环每执行一次,最内层循环需要执行 100 次,那总共程序想要从这三个循环中出来,就需要
执行 100100100 次,也就是 n 的立方,所以这段代码的时间复杂度是 O(n/3)
2.3.2.4 对数阶
由于每次 i2 之后,就距离 n 更近一步,假设有 x 个 2 相乘后大于 n,则会退出循环。由于是 2^x=n,
得到 x=log(2)n,所以这个循环的时间复杂度为 O(logn);
对于对数阶,由于随着输入规模 n 的增大,不管底数为多少,他们的增长趋势是一样的,所以
我们会忽略底数.
2.3.2.5 常数阶
一般不涉及循环操作的都是常数阶、因为它不会随着 n 的増长而増加操作次数。
例如
上述代码,不管输入规模 n 是多少,都执行 2 次,根据大 0 推导法则,常数用 1 来替换,所以
上述代码的时间复杂度为 O(1),下面是对常见时间复杂度的一个总结:
他们的复杂程度从低到高依次为:
O(1)<O(logn)<O(n)<O(nlogn)<O(n^2)<O(n^3)
根据前面的折线图分析,我们会发现,从平方阶开始,随着输入规模的增大,时间成本会急剧 增大,所以,我们的算法,尽可能的追求的是 O(1),O(logn),O(n),O(nlogn)这几种时间复杂度,而 如果发现算法的时间复杂度为平方阶、立方阶或者更复杂的,那我们可以分为这种算法是不可取的,需要优化。
2.4 平均和最坏时间复杂度
平均时间复杂度是指所有可能的输入实例均以等概率的出现情况下得到算法的运行时间 .
最坏时间复杂度,一般讨论的时间复杂度均是最坏情况下的时间复杂度,这样做的原因是最坏 情况下的时间复杂度是算法在任何输入实例上运行的界限,这就保证了算法的运行时间不会比 最坏情况更长。
平均时间复杂度和最坏时间复杂度是否一样,这就需要根据算法不同而不同了
2.5 算法的空间复杂度分析
1、基本数据类型内存占用情况:
2、 计算机访问内存的方式都是一次一个字节
3、一个引(机器地址)需要 8 个字节表示;
例如 Date date - new Date(,Qdate 这个变量紧要占用 8 个字节来表示
4、创建一个对象,比如 new Date(),除了 Date 对象内部存储的数据(例如年月日等信息)
占用的内存,该对象本身也有内存开销,每个对象的自身开销是 16 个字节,用来保存对
象的头信息。
5、一般内存的使用,如果不够 8 个字节,都会被自动填充为 8 字节:
6. java 中数组被被限定为对象,他们一般都会因为记录长度而需要额外的内存,一个原 始数据类型的数组一般需要 24 字节的头信息(16 个自己的对象开销,4 字节用于保存长度以及 4 个填充字节)再加上保存值所需的内存
2.6算法的空间复杂度
三.排序算法
3.1排序算法介绍
排序也称之为排序算法(Sort Algorithm),是讲一组数据以指定的顺序进行排列的过程。Java 提供了一个接口 Comparable 就是用来定义排序规则的
3.2 基数排序
3.2.1 介绍
基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”
思想是将整数按位数切割成不同的数字,然后按每个位数分别比较。
3.2.2 思想
讲所有的待比较数值统一设置为同样的数位长度,位数比较短的数前面补零,然后从最低位开始依次进行一次排序,这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
例如:
Int[] array = {53,3,542,728,14,214};
1、首先确定最大数是 728,(这个一定要确定)
2、确定位数后,不足十位和百位的在前面补 0
3、比较数组中的个位数,按照顺序放到对应的桶中,每个桶都是一个一维数组,全部放进去
后,再取出来重新排序。
按照个位数排序放进桶中后得到的顺序为 542,53,3,14,214,728
4、然后再依次比较十位数,放到对应的桶中,没有十位的前面补 0
按照十位数放入桶中得到的顺序为:3,14,214,728,542,53
5、依次比较百位数,放到对应的桶中去,没有百位的补 0
按照百位数放入桶中的顺序是:3,14,53,214,542,728
3.2.3 算法实现
package com.lans.SortAlgorithm;
import java.util.ArrayList;
import java.util.Arrays;
/*
* @author: lans
* @date: 2024/12/22
* @name: 刘宇
* 基数排序
*/
//基数排序
public class BasicSort {
private static final int[] array1 = {53,3,542,728,14,214};
public static int[] sort(int[] array){
//1.先找出最大值
int max = 0;
for(int temp:array){
if(temp>max){
max=temp;
}
}
//2.计算最大值是几位数
int maxDigit = 0;
while(max!=0){
max/=10;
maxDigit++;
}
//3.创建0-9个桶
ArrayList<ArrayList<Integer>> buckets = new ArrayList<>();
for(int i=0;i<10;i++){
buckets.add(new ArrayList<>());
}
//第一次取个位数 然后十位 百位...
int mold = 10;
int div=1;
//控制一共取多少位数
//如153取出个位153%10=3/1=3 53取出个位%10/1=3 3取出个位%10/1=3
//153取十位 153%100=53/10=5 53取出十位%100=53/10=3 3取出十位3%100=3/10=0
for(int i=0;i<maxDigit;i++,mold*=10,div*=10){//每一次循环代表取个位 十位 ...
for (int k : array) {
int num = (k % mold) / div;
buckets.get(num).add(k);//将对应元素放入桶中
}
//所有的取出放桶已完成 按序取出即是排序顺序
int index=0;
for (ArrayList<Integer> list : buckets) {//遍历桶集合
for (Integer integer : list) {//遍历集合中的元素
array[index++] = integer;//存入数组
}
list.clear();//清空桶
}
}
return array;
}
public static void main(String[] args) {
int[] array11 = sort(array1);
System.out.println(Arrays.toString(array11));
}
}
3.3 冒泡排序
3.3.1 介绍
冒泡排序的思想是通过对待排序序列从前往后依次比较相邻元素值,若发现逆序则交换,使值 较大的元素从前逐步移向后面,就想水中气泡。
3.3.2 思想
假设待排序序列为(5,1,4,2,8),如果采用冒泡排序对其进行升序(由小到大)排序,则整个排序过
程如下所示:
第一轮排序,此时整个序列中的元素都位于待排序序列,依次扫描每对相邻的元素,并对顺序
不正确的元素对交换位置,整个过程如图所示。
从图可以看到,经过第一轮冒泡排序,从待排序序列中找出了最大数 8,并将其放到了待排序
序列的尾部,并入已排序序列中。
第二轮排序,此时待排序序列只包含前 4 个元素,依次扫描每对相邻元素,对顺序不正确的元
素对交换位置,整个过程如图所示。
经过第二轮冒泡排序,从待排序序列中找出最大值 5,将其放入待排序序列尾部
第三轮排序,此时待排序序列包含前 3 个元素,依次扫描每对相邻元素,对顺序不正确的元素
对交换位置,整个过程如图所示
经过第三轮排序,找出最大值 4,将其放入待排序序列中
第四轮排序,此时待排序序列包含前 2 个元素,对其进行冒泡排序
经过第三轮排序,找出最大值 2,将其放入待排序序列中
第四轮排序,此时待排序序列只有 1 个元素,无需再进行相邻元素比较,因此直接将其并入已
排序序列中即可。
特点:
1、 需要循环 array.length-1 次 外层循环
2、 每次排序的次数逐步递减
3、 也可能存在本次排序没有发生变化
3.2.3 算法实现
package com.lans.SortAlgorithm;
import java.util.Arrays;
/*
* @author: lans
* @date: 2025/1/10
* @name: 刘宇
* 冒泡排序
*/
public class BubblingSort {
public static void main(String[] args) {
int[] arrays = new int[]{3,1,3,5,7,5,9,8};
int[] arrays1 = sort(arrays);
System.out.println(Arrays.toString(arrays1));
}
public static int[] sort(int[] arrays){
for(int i=0;i<arrays.length-1;i++){
boolean flag = false;
for(int j = 0;j<arrays.length-i-1;j++){
if(arrays[j]>arrays[j+1]){
int temp = arrays[j];
arrays[j] = arrays[j+1];
arrays[j+1] = temp;
flag=true;
}
}
if(!flag){
break;
}
}
return arrays;
}
}
3.4快速排序
3.4.1介绍
快速排序是一种高效的排序算法,由于其时间复杂度优于大部分的排序算法,因而命名为快速排序。在Java程序中,JDK中的Arrays.sort()方法的内部实现就应用到了快速排序的思想。
3.4.2思想
快速排序的中心思想是在待排序序列中选择一个基准值(pivot),然后将小于基准值的数字放在基准值左边,大于基准值的数字放在基准值右边,接着对左右两边递归排序。整个排序过程中,最关键的部分就是寻找基准值在待排序序列中的索引位置,这通常通过分区操作来实现。
3.4.3算法实现
package com.lans.SortAlgorithm;
import java.util.Arrays;
/*
* @author: lans
* @date: 2025/1/15
* @name: 刘宇
*/
public class QuickSort {
public static void main(String[] args) {
int[] arr = {1,4,12,325,41231231,3,2,432,3131,43245};
quickSort(arr, 0, arr.length-1);
System.out.println(Arrays.toString(arr));
}
public static void quickSort(int[] arr,int startIndex,int endIndex) {
if(startIndex>=endIndex) {
return;
}
int pIndex = partition(arr,startIndex,endIndex);
quickSort(arr,startIndex,pIndex-1);
quickSort(arr,pIndex+1,endIndex);
}
public static int partition(int[] arr,int startIndex,int endIndex){
int p = arr[startIndex];
int l = startIndex;
int r = endIndex;
while(l!=r){
//右边指针 当r在l右侧且该指针指向的元素大于基准元素时左移
while((l<r)&&(arr[r]>p)){
r--;
}
//左指针 当l还在r右边且该指针指向的元素小于基准元素时右移
while((l<r)&&(arr[l]<=p)){
l++;
}
if(l<r){
int temp = arr[l];
arr[l]=arr[r];
arr[r]=temp;
}
}
//交换基准元素和重合的元素
arr[startIndex]=arr[l];
arr[l]=p;
return l;
}
}
3.5插入排序
3.5.1介绍
插入排序(Insertion Sort)是一种简单直观的排序算法,适用于少量数据的排序,其时间复杂度在最优情况下为O(n),而在最坏情况下为O(n^2)。尽管它在大规模数据上效率不高,但由于其实现简单,在一些特定情况下仍然非常有用。
3.5.2核心思想
插入排序的核心思想是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。具体来说,插入排序的工作方式如下:
- 初始状态:假设数组的第一个元素已经排序,即认为它是一个长度为1的有序序列。
- 遍历未排序部分:从数组的第二个元素开始,逐个取出每一个元素,将其插入到已排序序列的适当位置,直到所有元素都插入完毕。
- 插入操作:在插入元素时,从已排序序列的末尾开始向前扫描,找到该元素应该插入的位置,并将其插入。插入位置的确定是通过逐个比较来实现的,如果找到的元素比待插入元素小,则将该元素向后移动一位。
3.5.3算法实现
package com.lans.SortAlgorithm;
import java.util.Arrays;
/*
* @author: lans
* @date: 2025/1/20
* @name: 刘宇
*/
public class InsertSort {
public static void main(String[] args) {
int[] arrays = new int[]{2, 6, 4, 1, 3, 7, 5};
insertSort(arrays);
System.out.println(Arrays.toString(arrays));
}
public static void insertSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int key = arr[i]; // 当前要插入的元素
int j = i - 1;//已经排序好的元素-1
// 将arr[i]插入到已排序序列arr[0..i-1]中
while (j >= 0 && arr[j] > key) {
//跳出循环的情况:j=-1/j已经到该待的位置
arr[j + 1] = arr[j]; // 将大于key的元素向后移动
j = j - 1;//索引向左移动 与更前面的元素进行比较
}
arr[j + 1] = key; // 插入key到正确的位置
}
}
}
3.6选择排序
3.6.1介绍
第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置, 然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。选择排序是不稳定的排序方法。
3.6.2算法实现
package com.lans.SortAlgorithm;
import java.util.Arrays;
/*
* @author: lans
* @date: 2025/1/21
* @name: 刘宇
*/
public class SelectSort {
public static void main(String[] args) {
int[] array = {8, 3, 2, 1, 7, 4, 6, 5};
System.out.println("排序前: " + Arrays.toString(array));
selectSort(array);
System.out.println("排序后: " + Arrays.toString(array));
}
public static void selectSort(int[] arr) {
for(int i=0;i<arr.length;i++) {
//假设最小元素
int minIndex = i;
//二层寻找最小值
for(int j=i+1;j<arr.length;j++) {
if(arr[j] < arr[minIndex]) {
//记录新的最小值下标
minIndex = j ;
}
}
//将最小值与假设最小值进行交换
int temp = arr[minIndex];
arr[minIndex]= arr[i];
arr[i] = temp;
}
}
}
3.7希尔排序**
3.7.1 介绍
希尔排序(Shell's Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是插入排序算法的一种更高效的改进版本。 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便
终止。
3.7.2 算法思想
- 希尔排序方法 (
shellSort
):
-
- 初始化增量
gap
为数组长度的一半,之后每次循环将增量减半,直到增量为1。 - 对每个增量子数组进行插入排序。外层循环变量
i
从增量gap
开始,内层循环变量j
从i
开始向前遍历,直到j < gap
或者当前元素小于等于其前gap
个位置的元素。 - 在内层循环中,如果当前元素
arr[j]
大于其前gap
个位置的元素arr[j - gap]
,则将arr[j - gap]
向后移动一个增量位置,并将j
减去增量gap
,继续比较。如果当前元素小于等于其前gap
个位置的元素,则将当前元素插入到正确的位置。
- 初始化增量
3.7.3 算法实现
package com.lans.SortAlgorithm;
import java.util.Arrays;
/*
* @author: lans
* @date: 2025/1/24
* @name: 刘宇
*/
public class ShellSort {
public static void main(String[] args) {
int[] arr = {12, 34, 54, 2, 3};
System.out.println("排序前的数组:"+ Arrays.toString(arr));
shellSort(arr);
System.out.println("排序后的数组:"+ Arrays.toString(arr));
}
//负责分组
public static void shellSort(int[] arr){
int gap= arr.length;
while(gap>1){
gap/=2;
shellSort(arr,gap);
}
}
//负责分组后,对每组元素进行插入排序
public static void shellSort(int[] arr,int gap) {
//非空判断 避免运行时异常
if(arr == null){
return;
}
//i是组的个数
for (int i = gap; i < arr.length; i++) {
int temp = arr[i];
int j;
for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap) {
arr[j + gap] = arr[j]; // 移动元素
}
arr[j + gap] = temp; // 放置temp在正确的位置
}
}
}
3.8 归并排序
3.8.1 介绍
归并排序(Merge Sort)是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分 治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的 序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
3.8.2 示意图
我们需要将两个已经有序的子序列合并成一个有序序列,比如上图最后一次合并,将[2,4,5,6] 和[1,3,7,8]已经有序的子序列合并最终序列[1,2,3,4,5,6,7,8]
3.8.3 算法实现
递归方式实现
package com.lans.SortAlgorithm;
import java.util.Arrays;
/**
* @author: lans
* @date: 2025/2/11
* @name: 刘宇
*/
public class MergeSort {
public static void main(String[] args) {
int[] arr = {5,2,4,1,56,87,212,3};
int[] temp = new int[arr.length];
mSort(arr,0,arr.length-1,temp);
System.out.println(Arrays.toString(arr));
}
/**
* 向下分解的过程
* @param arr 无序序列
* @param left 左指针
* @param right 右指针
* @param temp 临时数组
*/
public static void mSort(int[] arr,int left,int right,int[] temp){
if(left<right){
int mid = (left+right)/2;
//向左进行分解
mSort(arr,left,mid,temp);
//向右进行分解
mSort(arr,mid+1,right,temp);
//无法分解后 归并
mergeSort(arr,left,right,mid,temp);
}
}
//开始归并
public static void mergeSort(int[] arr,int left,int right,int mid,int[] temp){
int i = left;//左边序列的开始位置
int j = mid+1;//右边序列的开始位置
int e = 0;//用来记录临时数组的下标
while(i<=mid&&j<=right){
if(arr[i]<=arr[j]){
temp[e] = arr[i];
e++;
i++;
}else{
temp[e] = arr[j];
e++;
j++;
}
}
//将左边有序序列剩余的元素平移到临时数组中来
while(i<=mid){
temp[e] = arr[i];
e++;
i++;
}
//将右边的有序序列剩余的元素平移到临时数组中来
while(j<=right){
temp[e] = arr[j];
e++;
j++;
}
//将临时数组元素拷贝回原数组
e= 0 ;
while(left<=right){
arr[left] = temp[e];
left++;
e++;
}
}
}
四.数据结构
4.线性表
线性表是最基本,最简单,也是最常用的一种数据结构,一个线性表是n具有相同特性的数据元素的有限序列.
前驱元素:
若 A 元素在 B 元素的前面,则称 A 为 B 的前驱元素
后继元素:
若 B 元素在 A 元素的后面,则称 B 为 A 的后继元素
线性表的特征:
数据元素之间具有"一对一"的逻辑关系.
1.第一个数据元素没有前驱,这个数据元素被称为头结点;
2.最后一个数据元素没有后继,这个数据元素被称为尾结点;
3.除了第一个和最后一个数据元素外,其他数据元素有且仅有一个前驱和一个后继。
线性表的分类:
线性表中数据存储的方式可以是顺序存储,也可以是链式存储,按照数据的存储方式不同,可以把线性表分为顺序表和链表。
4.1顺序表
顺序表是以数组形式保存的线性表.地址连续 依次存储.
4.1.1 顺序表API设计
package com.lans.linear;
/**
* @author: lans
* @date: 2025/2/13
* @name: 刘宇
*/
public class SequenceList<T> {
private T[] elses; // 存储元素的数组
private int N; // 线性表长度
/**
*
* @param capacity 容量大小
*/
public SequenceList(int capacity) {
this.elses = (T[])new Object[capacity];
this.N=0;
}
/**
* 清空线性表
*/
public void clear(){
this.N=0;
}
/**
* 判断是否为空
* @return
*/
public boolean isEmpty(){
return this.N==0;
}
/**
* 获取线性表中元素的个数
* @return
*/
public int length(){
return this.N;
}
/**
* 读取并放回线性表中的第i个元素的值
* @param i
* @return
*/
public T get(int i){
return elses[i];
}
/**
* 在线性表的第i个元素之间插入一个值为t的数据元素
* @param i
* @param t
*/
public void insert(int i,T t){
for(int index = N;index>i;index--){
elses[index] = elses[index-1];
}
elses[i]=t;
N++;
}
/**
* 向线性表中添加一个元素t
* @param t
*/
public void insert(T t){
elses[N++] = t;
}
/**
* 删除并返回线性表中的第i个元素
* @param i
* @return
*/
public T remove(int i){
T current = elses[i];
for(int index = i;index<N-1;index++){
elses[index] = elses[index+1];
}
N--;
return current;
}
/**
* 返回线性表中首次出现的指定的数据元素的位序号 若不存在返回-1
* @param t
* @return
*/
public int indexOf(T t){
for(int i=0;i<N;i++){
if(elses[i]==t){
return i;
}
}
return -1;
}
//----------------------------------------------------
//测试
public static void main(String[] args) {
SequenceList<String > stringSequenceList = new SequenceList<>(5);
stringSequenceList.insert("猪八戒");
stringSequenceList.insert("淼淼");
stringSequenceList.insert("宇宇");
stringSequenceList.insert(1,"唐僧");
stringSequenceList.remove(0);
String s = stringSequenceList.get(0);
System.out.println(s);
}
}
4.1.2顺序表遍历
一般作为容器存储数据,都需要向外部提供遍历的方式,因此我们需要给顺序表提供遍历 方式。
在 java 中,遍历集合的方式一般都是用的是 foreach 循环,如果想让我们的SequenceList 也能 支持 foreach 循环,则需要做如下操作:
1.让 SequenceList 实现 terable 接口,重写 iterator 方法;
2.在 SequenceList 内部提供一个内部类 Sterator,实现 Iterator 接口,重写 hasNext 方法和 next 方法;
使用迭代器的方式遍历集合
package com.lans.linear;
import java.util.Iterator;
/**
* @author: lans
* @date: 2025/2/13
* @name: 刘宇
*/
public class SequenceList<T> implements Iterable{
public Iterator iterator() {
return new MyIterable();
}
//自定如何返回当前顺序表中的元素
private class MyIterable implements Iterator{
//制定指针
private int cusor;
public MyIterable(){
cusor = 0;
}
//判断是否还有下一个元素
public boolean hasNext(){
return cusor<N;
}
//获取下一个元素
public Object next(){
return elses[cusor++];
}
}
private T[] elses; // 存储元素的数组
private int N; // 线性表长度
/**
*++
* @param capacity 容量大小
*/
public SequenceList(int capacity) {
this.elses = (T[])new Object[capacity];
this.N=0;
}
/**
* 清空线性表
*/
public void clear(){
this.N=0;
}
/**
* 判断是否为空
* @return
*/
public boolean isEmpty(){
return this.N==0;
}
/**
* 获取线性表中元素的个数
* @return
*/
public int length(){
return this.N;
}
/**
* 读取并放回线性表中的第i个元素的值
* @param i
* @return
*/
public T get(int i){
return elses[i];
}
/**
* 在线性表的第i个元素之间插入一个值为t的数据元素
* @param i
* @param t
*/
public void insert(int i,T t){
for(int index = N;index>i;index--){
elses[index] = elses[index-1];
}
elses[i]=t;
N++;
}
/**
* 向线性表中添加一个元素t
* @param t
*/
public void insert(T t){
elses[N++] = t;
}
/**
* 删除并返回线性表中的第i个元素
* @param i
* @return
*/
public T remove(int i){
T current = elses[i];
for(int index = i;index<N-1;index++){
elses[index] = elses[index+1];
}
N--;
return current;
}
/**
* 返回线性表中首次出现的指定的数据元素的位序号 若不存在返回-1
* @param t
* @return
*/
public int indexOf(T t){
for(int i=0;i<N;i++){
if(elses[i]==t){
return i;
}
}
return -1;
}
//----------------------------------------------------
//测试
public static void main(String[] args) {
SequenceList<String > stringSequenceList = new SequenceList<>(5);
stringSequenceList.insert("猪八戒");
stringSequenceList.insert("淼淼");
stringSequenceList.insert("宇宇");
stringSequenceList.insert(1,"唐僧");
stringSequenceList.remove(0);
String s = stringSequenceList.get(0);
System.out.println(s);
Iterator iterator = stringSequenceList.iterator();
//使用迭代器遍历集合
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
4.1.3 顺序表的容量可变
/**
* 在线性表的第i个元素之间插入一个值为t的数据元素
* @param i
* @param t
*/
public void insert(int i,T t){
//扩容
if(N==elses.length){
resize(2*elses.length);
}
for(int index = N;index>i;index--){
elses[index] = elses[index-1];
}
elses[i]=t;
N++;
}
/**
* 向线性表中添加一个元素t
* @param t
*/
public void insert(T t){
if(N==elses.length){
resize(2*elses.length);
}
elses[N++] = t;
}
/**
* 删除并返回线性表中的第i个元素
* @param i
* @return
*/
public T remove(int i){
T current = elses[i];
for(int index = i;index<N-1;index++){
elses[index] = elses[index+1];
}
N--;
if(N<elses.length/4){
resize(elses.length/2);
}
return current;
}
/**
* 修改数组大小
*/
private void resize(int newSize){
T[] temp = elses;
elses = (T[]) new Object[newSize];
for(int i = 0;i<N;i++){
temp[i] = elses[i];
}
}
4.2栈
4.2.1栈的介绍(有底容器)
是一种组织数据的方式
栈是限制插入和删除只能在一个位置上进行的线性表。其中,允许插入和删除的一端位于 表的末端,叫做栈顶(top),不允许插入和删除的另一端叫做栈底(bottom)。对栈的基本 操作有PUSH(压栈)和POP(出栈),前者相当于表的插入操作(向栈顶插入一个元素), 后者则是删除操作(删除一个栈顶元素)。栈是一种后进先出(LIFO)的数据结构,最先被删除的是最近压栈的元素
栈实现
由于栈是一个表,因此任何实现表的方法都可以用来实现栈。主要有两种方式,链表实现 和数组实现。
4.2.2 链表实现栈
package com.lans.linear;
import java.util.Iterator;
/**
* @author: lans
* @date: 2025/2/23
* @name: 刘宇
*/
public class Stack <T> implements Iterable<T> {
@Override
public Iterator<T> iterator() {
return new MyIterator();
}
private class MyIterator implements Iterator<T> {
private Node n = head;
@Override
public boolean hasNext() {
return n.next!=null;
}
@Override
public T next() {
Node node = n.next;
n = n.next;
return node.item;
}
}
private Node head;
private int N;
public Stack(){
head = new Node(null,null);
N=0;
}
//判断栈是否为空
public boolean isEmpty(){
return N==0;
}
//获取栈中的元素个数
public int size(){
return N;
}
//弹出栈顶元素 获取栈的第一个元素
public T pop(){
Node firstNode = head.next;
if(firstNode==null){
return null;
}
//删除第一个节点 第一个节点会因为JVM的垃圾回收机制回收空间
head.next = head.next.next;
N--;
return firstNode.item;
}
//向栈中压入元素
public void push(T t){
Node firstNode = head.next;
Node node = new Node(t,firstNode);
head.next = node;
N++;
}
private class Node{
private T item;
private Node next;
public Node(T item,Node next){
this.item = item;
this.next = next;
}
}
public static void main(String[] args) {
Stack<String> stack = new Stack<String>();
stack.push("王嘉淼");
stack.push("刘宇");
stack.push("哈基米");
stack.push("缅因 耄耋");
stack.pop();
for(String str:stack){
System.out.println(str);
}
}
}
4.2.3数组实现栈
用指针来记录下标
package com.lans.linear;
/**
* @author: lans
* @date: 2025/2/23
* @name: 刘宇
*/
public class ArrayStack {
//栈
private int[] stackArr;
//大小
private int maxStack;
//指针
private int top = -1;
public ArrayStack(int maxStack) {
this.maxStack = maxStack;
this.stackArr = new int[maxStack];
}
//判断栈是否已满
public boolean isFull(){
return top == maxStack-1;
}
// 判断是不是空的
public boolean isEmpty(){
return top == -1;
}
//压栈
public void push(int item){
if (isFull()) {
System.out.println("Stack is full");
}else{
stackArr[++top] = item;
}
}
//弹栈
public int pop(){
if (isEmpty()) {
throw new RuntimeException("Stack is empty,没哦有数据");
}
//记录
int value = stackArr[top];
//长度-短
top--;
return value;
}
//查询栈中的数据
public void show(){
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
for(int i = 0;i<top;i++){
System.out.printf("stack[%d]=%d\n", i, stackArr[i]);
}
}
public static void main(String[] args) {
ArrayStack arrayStack = new ArrayStack(5);
arrayStack.push(10);
arrayStack.push(11);
arrayStack.push(12);
arrayStack.push(13);
arrayStack.push(15);
arrayStack.push(14);
arrayStack.show();
}
}
4.2.4 括号匹配问题
给定一个字符串,里边可能包含"()"小括号和其他字符,请编写程序检查该字符串的中的小括号是否成对出现
1.扫描字符串,如果发现存在左括号"("
2.扫描发现存在右括号,去栈中弹出一个对应的左括号.如果栈中不存在左括号,说明不匹配.
package com.lans.linear;
import java.util.Iterator;
/**
* @author: lans
* @date: 2025/2/23
* @name: 刘宇
*/
public class Stack <T> implements Iterable<T> {
@Override
public Iterator<T> iterator() {
return new MyIterator();
}
private class MyIterator implements Iterator<T> {
private Node n = head;
@Override
public boolean hasNext() {
return n.next!=null;
}
@Override
public T next() {
Node node = n.next;
n = n.next;
return node.item;
}
}
private Node head;
private int N;
public Stack(){
head = new Node(null,null);
N=0;
}
//判断栈是否为空
public boolean isEmpty(){
return N==0;
}
//获取栈中的元素个数
public int size(){
return N;
}
//弹出栈顶元素 获取栈的第一个元素
public T pop(){
Node firstNode = head.next;
if(firstNode==null){
return null;
}
//删除第一个节点 第一个节点会因为JVM的垃圾回收机制回收空间
head.next = head.next.next;
N--;
return firstNode.item;
}
//向栈中压入元素
public void push(T t){
Node firstNode = head.next;
Node node = new Node(t,firstNode);
head.next = node;
N++;
}
private class Node{
private T item;
private Node next;
public Node(T item,Node next){
this.item = item;
this.next = next;
}
}
public static void main(String[] args) {
// Stack<String> stack = new Stack<String>();
// stack.push("王嘉淼");
// stack.push("刘宇");
// stack.push("哈基米");
// stack.push("缅因 耄耋");
// stack.pop();
// for(String str:stack){
// System.out.println(str);
// }
boolean match = isMatch("((布置后无)");
System.out.println(match);
}
public static boolean isMatch(String str){
//1.扫描字符串
Stack<String> stack = new Stack<String>();
for(int i=0;i<str.length();i++){
//2.从左向右依次获取每一个字符
String current = str.charAt(i)+"";
//3.判断当前字符串是否存在左括号
if(current.equals("(")){
//4.如果是左括号,则放入栈中去
stack.push(current);
}else if(current.equals(")")){
//5.如果是右括号,则去弹出对应的左括号
String s = stack.pop();
//6.如果弹出来是空值,则表示该字符串不匹配
if(s==null){
return false;
}
}
}
//7.如果字符串遍历结束,栈中依然还存在左括号,则表示字符串不匹配
if(stack.size()==0){
//8.字符串遍历结束,对应的有括号都能弹出左括号,并且栈中直至为null,表示匹配成功
return true;
}else{
return false;
}
}
}
4.3链表
是组织数据的一种方式
4.3.1链表(LinkedList)介绍
链表是一种物理存储单元上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表中的 指针链接实现的
特点:
1. 链表是以结点形式存储的,是链式存储
2. 每个结点包含data区域和next区域
3. 如上图各个结点并不是连续存储的
4. 链表分带头结点链表和没有带头结点链表,根据实际的需求来确定 带头结点链表逻辑结构
查询多 :使用顺序表 插入删除多:使用链表
package com.lans.linear;
/**
* @author: lans
* @date: 2025/2/18
* @name: 刘宇
*/
public class Node<T> {
public T item;
public Node next;
public Node(T item,Node next) {
this.item = item;
this.next = next;
}
}
4.3.2单向链表
单向链表是链表的一种,它由多个结点组成,每个结点都由一个数据域和一个指针域组成,数据域用来存储数据, 指针域用来指向其后继结点。链表的头结点的数据域不存储数据,指针域指向第一个真正存储数据的结点。
案例一
package com.lans.linear;
import java.util.Iterator;
/**
* @author: lans
* @date: 2025/2/18
* @name: 刘宇
*/
//对数据进行查询 删除 等操作
public class LinkList <T> implements Iterable<T>{
//记录头节点
private Node head;
//记录链表的长度
private int N;
public LinkList(){
head = new Node(null,null);
N= 0 ;
}
@Override
public Iterator<T> iterator() {
return null;
}
private class LIterator implements Iterator<T>{
@Override
public boolean hasNext() {
return false;
}
@Override
public T next() {
return null;
}
}
private class Node{
//存储的T类型元素
public T item;
//下一个节点
public Node next;
public Node(T item,Node next){
this.item = item;
this.next = next;
}
}
//清空线性表元素
public void clear(){
head.next = null;
N=0;
}
//判断是否为空
public boolean isEmpty(){
return N==0;
}
//线性表长度
public int length(){
return N;
}
//读取并返回线性表中的第i个元素
public T get(int i){
if(i<0||i>N){
throw new RuntimeException("索引不合法");
}
Node n = head.next;
for(int index=0;index<i;index++){
n = n.next;
}
return n.item;
}
//向线性表中添加一个一个元素
public void insert(T i){
//找最后一个元素
Node n = head;
while(n.next!=null){
n = n.next;
}
Node node = new Node(i,null);
n.next = node;
N++;
}
//向指定位置i处,添加元素t
public void insert(int i,T t){
//创建新节点
Node newNode = new Node(t, null);
if(i<0||i>=N){
throw new RuntimeException("位置不合法");
}
//特殊情况 i==0
if(i==0){
newNode.next = head.next;
head = newNode;
N++;
return ;
}
//找到要插入新节点之前的那个节点
Node current = head;
for(int index = 0;index<=i-1;index++){
current = current.next;
}//0123456 3
newNode.next = current.next;
current.next = newNode;
N++;
}
//删除并返回线性表中的第i个元素
public T remove(int i){
if(i<0||i>N){
throw new RuntimeException("位置不合法");
}
Node n = head;
for(int index = 0;index<=i-1;i++){
n = n.next;
}
Node current = n.next;
//该节点的前一个节点指向后一个节点
n.next = current.next;
N--;
return current.item;
}
//返回线性表中首次出现的指定的数据元素的为序号 若不存在则返回-1
public int indexOf(T t){
Node node = head;
for(int index = 0;index<N-1;index++){
node = node.next;
if(node.item.equals(t)){
return index;
}
}
return -1;
}
}
案例二:
根据带有头部的单链表,实现商品的增删改查 并且可以针对商品已编号进行排序,完成排行榜
package com.lans.linear;
/**
* @author: lans
* @date: 2025/2/19
* @name: 刘宇
*/
public class GLinkedList{
//头结点
private GoodsNode node = new GoodsNode(0,"",0.0);
/**
* 添加节点
*/
public void add(GoodsNode goodsNode){
//判断是追加节点还是添加的第一个节点
GoodsNode temp = node;
while(true){
if(temp.next ==null){//最后一个或第一个
break;
}
temp = temp.next;
}
temp.next = goodsNode;
}
/**
* 添加节点根据编号进行排序
* @param goodsNode
*/
public void addByOrder(GoodsNode goodsNode){
//记录头结点
GoodsNode temp = node;
boolean flag = false;
while(true){
if(temp.next ==null){
break;
}
//添加新节点编号大于已经存在的编号
if(temp.next.gId>goodsNode.gId){
break;
}else if(temp.next.gId == goodsNode.gId){//已经存在的编号不能重复添加
flag = true;
break;
}
temp = temp.next;
}
if(flag){
System.out.println("该商品已存在 不能重复添加");
}else{
goodsNode.next=temp.next;
temp.next = goodsNode;
}
}
/**
* 根据编号进行修改
*/
public void updateNode(GoodsNode goodsNode){
if(node.next==null){
System.out.println("空链表");
return;
}
GoodsNode temp = node.next;
boolean flag = false;
while(true){
if(temp==null){
break;
}
if(temp.gId == goodsNode.gId){
flag = true;
break;
}
temp = temp.next;
}
if(flag){
temp.gName = goodsNode.gName;
temp.gPrice = goodsNode.gPrice;
}else{
System.out.println("未找到要修改的节点");
}
}
public void delNode(int gId){
GoodsNode temp = node;
boolean flag = false;
while(true){
if(temp==null){
break;
}
if(temp.next.gId ==gId){
flag = true;
break;
}
temp = temp.next;
}
if(flag){
temp.next = temp.next.next;
}else{
}
}
}
4.3.3 双向链表
双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继 和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和 后继结点
设计API
package com.lans.linear;
import java.util.Iterator;
/**
* @author: lans
* @date: 2025/2/19
* @name: 刘宇
*/
public class TwoWayLinkList<T> implements Iterable<T> {
@Override
public Iterator<T> iterator() {
return new MyIterator();
}
private class MyIterator implements Iterator<T>{
private Node node = head;
@Override
public boolean hasNext() {
return node.next!=null;
}
@Override
public T next() {
node = node.next;
return node.item;
}
}
public class Node{
public T item;
public Node pre;
public Node next;
public Node(T item, Node pre, Node next) {
this.item = item;
this.pre = pre;
this.next = next;
}
}
public TwoWayLinkList(){
head = new Node(null,null,null);
last = null;
N=0;
}
//头结点
private Node head;
//尾节点
private Node last;
//链表长度
private int N;
public void clear(){
last = null;
head.next = last;
head.pre = null;
head.item = null;
N=0;
}
public boolean isEmpty(){
return N==0;
}
public int length(){
return N;
}
public T get(int i){
if(i<0 || i>=N) {
throw new RuntimeException("该位置不和法");
}
Node temp = head.next;
for(int index=0;index<i;index++) {
temp = temp.next;
}
return temp.item;
}
public void insert(T t){
if(last ==null){
last = new Node(t,head,null);
head.next = last;
}else{
Node oldLast = last;
Node newLast = new Node(t, oldLast, null);
oldLast.next = newLast;
last = newLast;
}
N++;
}
public void insert(int i,T t){
if(i<0 || i>=N) {
throw new RuntimeException("位置不合理");
}
Node temp = head;
for(int index = 0;index<i;index++){
temp = temp.next;
}
Node current = temp.next;
Node node = new Node(t,temp,current);
current.pre = node;
temp.next = node;
N++;
}
public T remove(int i){
if(i<0 || i>=N) {
throw new RuntimeException("位置不合理");
}
Node temp = head;
for(int index = 0;index<i;index++){
temp = temp.next;
}
Node current = temp.next;
//获取要删除节点的下一个节点
Node node = current.next;
temp.next = node;
node.pre = temp;
N--;
return current.item;
}
//
public int indexOf(T t){
Node temp = head;
for(int index = 0;temp.next!=null;index++){
temp = temp.next;
if(temp.next.equals(t)){
return index;
}
}
return -1;
}
//获取头节点
public T getFirst(){
if(isEmpty()){
return null;
}
return head.next.item;
}
//获取尾节点/
public T getLast(){
if(isEmpty()){
return null;
}
return last.item;
}
public static void main(String[] args) {
TwoWayLinkList<String> list = new TwoWayLinkList<String>();
list.insert("孙悟空");
list.insert("虞姬");
list.insert("貂蝉");
list.insert("西施");
list.insert(2,"元歌");
System.out.println(list.remove(3));
System.out.println(list.getLast());
System.out.println(list.getFirst());
for(String str:list){
System.out.println(str);
}
}
}
4.3.4面试题-链表翻转
翻转单向链表
采用递归的形式,通过第一个节点找下一个节点直到下一个节点是null时 即可进入判断 将head.next=当下节点 一层层返回即可实现翻转
public void reverse(){
if(N!=0){
reverse(head.next);
}
}
public Node reverse(Node current){
if(current.next==null){
head.next = current;
return current;
}
Node pre = reverse(current.next);
pre.next = current;
current.next = null;
return current;
}
4.3.5面试题-快慢指针
快慢指针指的是定义两个指针,这两个指针的移动速度一块一慢,以此来制造出自己想要的差值,这个差值可以然 我们找到链表上相应的结点。一般情况下,快指针的移动步长为慢指针的两倍
案例:
请完善测试类Test中的getMid方法,可以找出链表的中间元素值并返回。 利用快慢指针,我们把一个链表看成一个跑道,假设a的速度是b的两倍,那么当a跑完全程后,b刚好跑一半,以 此来达到找到中间节点的目的。 如下图,最开始,slow与fast指针都指向链表第一个节点,然后slow每次移动一个指针,fast每次移动两个指针。
public String getMid(Node first){
Node slow = first;
Node fast = first;
while(fast !=null&&fast.next!=null){
fast = fast.next.next;
slow = slow.next;
}
return slow.item;
}
4.3.6 单向链表是否有环问题
两者相遇说明有环
public static boolean isCircle(Node first){
Node<String> slow = first;
Node<String> fast = first;
while(fast !=null&&fast.next!=null){
//双倍速度
fast = fast.next.next;
slow = slow.next;
if(fast.equals(slow)){
return true;
}
}
return false;
}
4.3.7 单向环形链表应用场景
约瑟夫问题(约瑟夫环)
设编号为1,2…,n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到 m 的那个人出列,它的下一位又从1开始报数,数到m的那个人出列,它的下一位又从1开始 报数,数到m的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列
解决约瑟夫问题分为两个步骤
第一步:做环形链表
第二步:解决约瑟夫问题
package com.lans.linear;
/**
* @author: lans
* @date: 2025/2/21
* @name: 刘宇
*/
public class BoyClircleLinkedList {
private Boy first = new Boy();
/**
* 添加形成环形链表
* @param nums
*/
public void addNode(int nums){
Boy temp = null;
for(int i=1;i<=nums;i++){
Boy boy = new Boy(i);
if(i==1){
first = boy;
first.next = first;//自己成环
temp = first;
}else{
temp.next = boy;
boy.next = first;//新的最后一个节点指向第一个
temp = boy;//每次记录最后一个节点
}
}
}
public void showBoy(){
if(first==null){
System.out.println("空链表");
return;
}
Boy temp = first;
while(true){
System.out.printf("小孩的编号是%d",temp.no);
//进行判断 不能无限循环的查下去
if(temp.next == first){
break;
}
temp=temp.next;
}
}
/**
* 执行淘汰节点
* @param starNo 从第几个孩子开始
* @param countNum 表示数了几下
* @param nums 总的孩子数量
*/
public void countBoy(int starNo,int countNum,int nums){
if(first==null||starNo<1||starNo>nums){
System.out.println("参数有误");
return;
}
Boy helper = first;
while(true){
//执行完一圈了
if(helper.next == first){
break;
}
helper=helper.next;
}
//设置从哪里开始数,把first和helper移动到该位置上去
for(int j=0;j<starNo-1;j++){
first = first.next;
helper = helper.next;
}
//开始出圈
while(true){
if(helper==first){
break;
}
for(int i=0;i<countNum-1;i++){
first = first.next;
helper=helper.next;
}
System.out.println("");
System.out.println("编号为"+first.no+"的出圈了");
first = first.next;
helper.next = first;
}
System.out.printf("编号为%d小孩未出圈",first.no);
}
public static void main(String[] args) {
BoyClircleLinkedList boyLinkedList = new BoyClircleLinkedList();
boyLinkedList.addNode(5);
boyLinkedList.showBoy();
boyLinkedList.countBoy(1,2,5);//从第一个开始,每两个
//小孩出去一次,一共有五个小孩
}
}
4.4 队列(常用)
4.4.1 队列介绍 (漏斗)
队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在 表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作 的端称为队尾,进行删除操作的端称为队头
1、队列是一个有序列表,可以用数组和链表来实现
2、队列先进先出,即是先入队列的数据最先被取出,后存入的数据后取出。
4.4.2 链表实现队列
package com.lans.linear;
import java.util.Iterator;
/**
* @author: lans
* @date: 2025/2/24
* @name: 刘宇
*/
public class Queue<T> implements Iterable<T> {
private Node head;
private Node first;
private Node last;
private int N;
public Queue() {
head = new Node(null,null);
last = null;
N=0;
}
@Override
public Iterator<T> iterator() {
return new MyIterator();
}
private class MyIterator implements Iterator<T>{
private Node n = head;
@Override
public boolean hasNext() {
return n.next !=null;
}
@Override
public T next() {
//获取当前节点
Node node = n.next;
n = n.next;
return node.item;
}
}
public boolean isEmpty(){
return N==0;
}
public int size(){
return N;
}
//从队列中拿出一个元素
public T dequeue(){
if(isEmpty()){
return null;
}
Node oldFirst = head.next;
head.next = oldFirst.next;
N--;
if (isEmpty()) {
last = null ;
}
return oldFirst.item;
}
//往队列中插入一个元素
public void enqueue(T t){
if(last==null){
last = new Node(t,null);
head.next = last;
}else{
Node oldLast = last;
last = new Node(t,null);
oldLast.next = last;
}
N++;
}
private class Node{
private T item;
private Node next;
public Node(T item, Node next) {
this.item = item;
this.next = next;
}
}
public static void main(String[] args) {
Queue<String> queue = new Queue<>();
queue.enqueue("马云");
queue.enqueue("马化腾");
queue.enqueue("马英睿");
queue.enqueue("马牛逼");
for(String str:queue){
System.out.println(str);
}
}
}
4.4.3 数组实现队列 数组模拟队列场景
package com.lans.linear;
/**
* @author: lans
* @date: 2025/2/24
* @name: 刘宇
*/
public class ArrayQueue {
//定义数组
private int[] array;
//数组大小
private int maxSize;
//头指针
private int headPoint;
//尾指针
private int lastPoint;
public ArrayQueue(int maxSize) {
this.maxSize = maxSize;
array = new int[maxSize];
headPoint = -1;
lastPoint = -1;
}
public boolean isEmpty() {
return headPoint == lastPoint;
}
//队列是否已满
public boolean isFull(){
return lastPoint == maxSize-1;
}
//往队列中添加数据
public void addQueue(int n){
if (isFull()) {
System.out.println("队列已满");
return;
}
lastPoint++;
array[lastPoint] = n;
}
//取元素
public int getQueue(){
if (isEmpty()) {
throw new RuntimeException("空队列");
}
headPoint++;
return array[headPoint];
}
//查询队列中的元素
public void list(){
if (isEmpty()) {
throw new RuntimeException("空队列");
}
for(int i = 0 ;i<array.length;i++){
System.out.printf("array[%d]=%d\n",i,array[i]);
}
}
//查看队列中的第一个元素
public int showHead(){
if (isEmpty()) {
throw new RuntimeException("空队列");
}
return array[++headPoint];
}
public static void main(String[] args) {
ArrayQueue queue = new ArrayQueue(5);
queue.addQueue(10);
queue.addQueue(11);
queue.addQueue(12);
queue.addQueue(13);
queue.addQueue(14);
System.out.println(queue.showHead());
queue.list();
}
}
4.5 符号表
符号表最主要的目的就是将一个键和一个值联系起来(键值对),符号表能够将存储的数据元素是一个键和一个值共同组成的 键值对数据,我们可以根据键来查找对应的值。 符号表中,键具有唯一性。
实现符号表可以通过链表和数组来实现.
package com.lans.linear;
import java.util.Iterator;
/**
* @author: lans
* @date: 2025/2/25
* @name: 刘宇
*/
public class SymbolTable implements Iterable{
private Node head;
private int N;
public SymbolTable() {
head = new Node(null,null,null);
N=0;
}
//根据键获取value值
public String getValue(String key){
Node n = head.next;
while(n.next!=null){
n = n.next;
if(n.key.equals(key)){
return n.value;
}
}
return null;
}
//添加键值对
public void put(String key,String value){
Node n = head;
while(n.next!=null){
n = n.next;
//当前符号表中存在该key,存在则覆盖原来的value
if(n.key.equals(key)){
n.value = value;
}
}
//不存在则添加新的键值对
Node oldFirst = head.next;
Node node = new Node(key,value,oldFirst);
head.next = node;
N++;
}
//根据键删除value值
public void delVal(String key){
Node n = head;
while(n.next!=null){
if(n.key.equals(key)){
n.next=n.next.next;
N--;
}
n = n.next;
}
}
public int size(){
return N;
}
@Override
public Iterator iterator() {
return new MyIterator();
}
private class MyIterator implements Iterator{
private Node n = head;
@Override
public boolean hasNext() {
return n.next != null;
}
@Override
public Object next() {
Node node= n.next;
n=n.next;
return node.key;
}
}
private class Node{
private String key;
private String value;
private Node next;
public Node(String key, String value, Node next) {
this.key = key;
this.value = value;
this.next = next;
}
}
public static void main(String[] args) {
SymbolTable symbolTable = new SymbolTable();
symbolTable.put("key1","东皇太一");
symbolTable.put("key2","司空震");
symbolTable.put("key3","元歌");
symbolTable.put("key4","马超");
System.out.println(symbolTable.getValue("key3"));
symbolTable.delVal("key3");
System.out.println(symbolTable.getValue("key3"));
}
}
4.6有序符号表
刚才实现的符号表,我们可以称之为无序符号表,因为在插入的时候,并没有考虑键值对的顺序,而在实际生活 中,有时候我们需要根据键的大小进行排序,插入数据时要考虑顺序
5. 非线性表
5.1 树*
为什么需要树中结构呢?
数组存储方式的解析:
1、优点:通过下标方式访问元素,速度快,对于有序数组还可以使用二分查找提高检索 速度。
2、缺点:如果要检索具体某个值,或者插入值会整理移动,效率较低
链式存储方式分析:
优点:在一定程序上对数组存储方式有优化,比如插入一个数值时,只需要讲插入点接到 链表中即可,删除效率也是同理效果好。
缺点:在进行检索时,效率仍然很低,检索某一个值时,需要从链表头一直做检索
树存储方式分析:
能提高数据存储,读取的效率,比如可以使用二叉树既可以保证数据检索速度,同时也可 以保证数据的插入,删除,修改的速度
左小右大.
5.1.1 二叉树
5.1.2 二叉树概念
二叉树(Binarytree)是树形结构的一个重要类型。许多实际问题抽象出来的数据结构往往是 二叉树形式,即使是一般的树也能简单地转换为二叉树,而且二叉树的存储结构及其算法都较 为简单,因此二叉树显得特别重要。二叉树特点是每个结点最多只能有两棵子树,且有左右之分
5.1.3 树示意图
5.1.4 二叉树介绍
1.每个节点最多只能有两个子节点的称为二叉树. 分为左节点和有节点
2、如果该二叉树的所有叶子结点都在最后一层,并且结点总数是2^n-1,n是层数,则我 们称之为满二叉树
3、如果该二叉树的所有叶子结点都在最后一层或者倒数第二层,而且最后一层的叶子结点在左边连续,倒数第二层的叶子结点在右边连续,我们称之为完全二叉树
5.1.4.1 设计二叉树节点
二叉树是一个一个的节点及其关系组成的
插入方法put
查询方法get
删除方法Delete
package com.lans.linear;
/**
* @author: lans
* @date: 2025/2/26
* @name: 刘宇
*
* 抽象类:可以存在非抽象方法
* 接口:抽象中的抽象 里面都是抽象方法 标准
*
*/
public class BinaryTree<K extends Comparable<K>,V> {
//树的根节点
private Node root;
//记录树的节点个数
private int N;
//插入一个节点
public void put(K key,V value){
root = put(root,key,value);
}
//往指定树中插入节点
public Node put(Node node,K key,V value){
//判断要插入的节点是否为空
if(node==null){
N++;
return new Node(key,value,null,null);
}
//cmp>0 说明传过来的key大于目标节点 放右边
//cmp<0 说明key小于目标节点 放左边
//cmp==0 说明找到目标节点 替换原来的值
int cmp = key.compareTo(node.key);
if(cmp>0){
node.right = put(node.right, key, value);
} else if(cmp<0){
node.left = put(node.left,key,value);
}else{
node.value = value;
}
return node;
}
//根据key找出对应的value值
public V get(K key){
//从根节点开始找
return get(root,key);
}
//从指定的树中找出对应的value值
public V get(Node node,K key){
if(node==null ){
return null;
}
int cmp = key.compareTo(node.key);
if(cmp>0){
return get(node.right,key);
}else if(cmp<0){
return get(node.left,key);
}else{
return node.value;
}
}
//删除value
public void delete(K key){
root = delete(root,key);
}
// 从指定的树中删除节点
public Node delete(Node node, K key) {
if (node == null) {
return null;
}
int cmp = key.compareTo(node.key);
if (cmp > 0) {
node.right = delete(node.right, key);
} else if (cmp < 0) {
node.left = delete(node.left, key);
} else {
N--;
// node就是要删除的节点
if (node.left == null) {
return node.right;
} else if (node.right == null) {
return node.left;
}
// 当前节点左右子树都存在
// 找到右子树的最小节点
Node minNode = node.right;
while (minNode!= null) {
minNode = minNode.left;
}
Node temp = node.right;
while(temp.left!=null){
if(temp.left.left==null){
temp.left = null;
}else{
temp = temp.left;
}
}
// 复制最小节点的值到当前节点
minNode.left = node.left;
minNode.right = node.right;
node = minNode;
}
return node;
}
//获取树中元素个数
public int size(){
return N;
}
//设计二叉树节点对象
private class Node{
//记录节点key
private K key;
//记录节点值value
private V value;
//记录左子节点
private Node left;
//记录右子节点
private Node right;
public Node(K key, V value, Node left, Node right) {
this.key = key;
this.value = value;
this.left = left;
this.right = right;
}
}
public static void main(String[] args) {
BinaryTree<String,String> binaryTree = new BinaryTree<>();
binaryTree.put("1","101");
binaryTree.put("2","102");
binaryTree.put("3","103");
binaryTree.put("4","104");
binaryTree.delete("3");
String s = binaryTree.get("2");
System.out.println(s);
System.out.println(binaryTree.size());
}
}
5.1.4.2查找二叉树中最小 最大的键
在某些情况下,我们需要查找出树中存储所有元素的键的最值
可通过这两个方法
//查找二叉树中最小的键
public K getMin(Node node){
if(node.left!=null){
return getMin(node.left);
}else{
return node.key;
}
}
public K getMin(){
return getMin(root);
}
//查找二叉树中最大节点
public K getMax(Node node){
if(node.right!=null){
return getMax(node.right);
}else{
return node.key;
}
}
public K getMax(){
return getMax(root);
}
5.1.5 二叉树应用案例
可以使用前序,中序,后序对下面的二叉树进行遍历
1、前序遍历:先输出父结点,再遍历左子树和右子树
2、中序遍历:先遍历左子树,再遍历父结点,再遍历右子树
3、后序遍历:先遍历左子树,再遍历右子树,最后遍历父结点
结论:看父结点输出顺序即是某序遍历
5.1.5.1 Node
package com.lans.binary;
/**
* @author: lans
* @date: 2025/2/27
* @name: 刘宇
*/
public class Node {
public int no;
public String name;
public Node left;
public Node right;
public Node(int no, String name) {
this.no = no;
this.name = name;
}
//前序遍历 先输出父节点再输出左节点再输出右节点
public void preSelect(){
System.out.println("父节点: "+this);
if(this.left!=null){
this.left.preSelect();
}
if(this.right!=null){
this.right.preSelect();
}
}
//中序遍历 先输出完左节点 再输出父节点 再输出右节点
public void infixSelect(){
if(this.left!=null){
this.left.infixSelect();
}
System.out.println("父节点: "+this);
if(this.right!=null){
this.right.infixSelect();
}
}
//后序遍历 先输出完左节点 再输出完右节点
public void postSelect(){
if(this.left!=null){
this.left.postSelect();
}
if(this.right!=null){
this.right.postSelect();
}
System.out.println("父节点: "+this);
}
@Override
public String toString() {
return "Node{" +
"no=" + no +
", name=" + name +
", left=" + left +
", right=" + right +
'}';
}
}
5.1.5.2 BinaryTree1
package com.lans.binary;
/**
* @author: lans
* @date: 2025/2/27
* @name: 刘宇
*/
public class BinaryTree1 {
public Node root;
public void preSelect(){
if(root!=null){
this.root.preSelect();
}else{
System.out.println("空二叉树");
}
}
public void infixSelect(){
if(root!=null){
this.root.infixSelect();
}else{
System.out.println("空二叉树");
}
}
public void postSelect(){
if(root!=null){
this.root.postSelect();
}else{
System.out.println("空二叉树");
}
}
}
5.1.6 堆
堆是计算机科学中一类特殊的数据结构的统称,堆通常可以被看做是一棵完全二叉树的数组
堆的特性:
1.它是完全二叉树,除了树的最后一层结点不需要是满的,其它的每一层从左到右都是满的,如果最后一层结点不是满的,那么要求左满右不满。
2.它通常用数组来实现。
具体方法就是将二叉树的结点按照层级顺序放入数组中,根结点在位置 1,它的子结点在位置 2 和 3,而子结点的子结点则分别在位置 4,5,6 和 7,以此类推。
\
由此可以推得:如果一个结点的位置为 k,则它的父结点的位置为[k/2],而它的两个子结点的位置则分别为 2k 和 2k+1。这样,在不使用指针的情况下,我们也可以通过计算数组的索引在树中上下移动:从 a[k]向上一层,就令 k 等于 k/2,向下一层就
令 k 等于 2k 或 2k+1。
3.每个结点都大于等于它的两个子结点。这里要注意堆中仅仅规定了每个结点大于等于它的两个子结点,但这两个子结点的顺序并没有做规定,跟我们之前学习的二叉查找树是有区别的。
insert 插入实现
堆是用数组完成数据元素的存储的,由于数组的底层是一串连续的内存地址,所以我们要往堆中插入数据,我们只能往数组中从索引 0 处开始,依次往后存放数据,但是堆中对元素的顺序是有要求的,每一个结点的数据要大于等于它的两个子结点的数据,所以每次插入一个元素,都会使得堆中的数据顺序变乱,这个时候我们就需要通过一些方法让刚才插入的这个数据放入到合适的位置。
在堆中插入元素后,还要满足堆的特性,保持数据层次上的顺序(由小到大,或者由大到小)。
对于一个大顶堆,如果新插入的元素在堆的最底层,需要对这个元素和上层的元素比较大小,
如果比上层的元素大则需要上浮操作,也就是一层一层的进行比较交换,如下图所示:
从堆中删除元素和插入元素同理,需要判断要删除的元素和上下层的大小关系,其实可以把上面插入的操作反过来理解,对应的操作是下沉操作,如下图所示:
package com.lans.binary;
/**
* @author: lans
* @date: 2025/2/28
* @name: 刘宇
*/
public class Heap <T extends Comparable<T>> {
//实现堆的数组
private T[] items;
//记录堆的大小
private int N;
public Heap(int capacity){
items = (T[])new Comparable[capacity+1];//N即代表最大索引
N=0;
}
//判断索引 i 除的元素与接触的元素的大小关系
//判断 i处的元素是否 小于 j处的元素
public boolean less(int i, int j){
return items[i].compareTo(items[j]) < 0;
}
//交换i和j处的元素
public void exch(int i, int j){
T temp = items[i];
items[i] = items[j];
items[j] = temp;
}
//k等于是新加入的节点索引
//上浮算法
public void swim(int k){
//当k=1时说明已经到达根节点
//0与1无需比较
while(k>1){
if(less(k/2,k)){//说明父节点小于子节点
exch(k/2,k);
}
k=k/2;
}
}
//平衡删除后的堆结构 k是索引
public void sink(int k){
//找到子节点中的最大值
while(k*2<=N){
int max;
if(2*k+1<=N){//存在右节点
if(less(2*k,2*k+1)){//比较左是否小于右
max = 2 * k + 1;
}else{
max = 2*k;
}
}else{//不存在最大即假设为左节点
max = 2*k;
}
//当前节点比左右子节点还大则不需要交换位置 结束循环即可
if(!less(k,max)){
break;
}
//当前节点小于最大值则交换位置
exch(k,max);
k=max;
}
}
//删除堆中的最大元素 根节点
public T delMax(){
T max = items[1];
exch(1,N);
items[N--]=null;//删除最大
//调整删除后的平衡位置
sink(1);
return max;
}
//新增
public void insert(T value){
this.items[++N]=value;
//新增的数进行上浮
swim(N);
}
}
进行测试
package com.lans.binary;
/**
* @author: lans
* @date: 2025/2/28
* @name: 刘宇
*/
public class TestHeap {
public static void main(String[] args) {
Heap<String> tHeap = new Heap<>(20);
tHeap.insert("5");
tHeap.insert("!");
tHeap.insert("G");
tHeap.insert("B");
tHeap.insert("T");
tHeap.insert("C");
tHeap.insert("7");
tHeap.insert("9");
tHeap.insert("2");
String del;
while((del = tHeap.delMax())!=null){
System.out.println("删除的元素"+del);
}
}
}
堆的排序:
将最大值和最小值不断进行交换然后放入数组,每次找根节点都需要下沉.
package com.lans.binary;
/**
* @author: lans
* @date: 2025/2/28
* @name: 刘宇
*/
public class HeapSort{
/*
设计两个数组
原数组:构造堆的元素所存放的数组
堆数组:实现堆结构的数组
*/
//创建堆数组
public static void createHeap(Comparable[]heap,Comparable[] source){
//拷贝数组
System.arraycopy(source,0,heap,1,source.length);
for(int i = (heap.length)/2;i>0;i--){
//下沉调整位置
sink(heap,i,heap.length-1);
}
}
public static void sort(Comparable[] source){
Comparable[] heap = new Comparable[source.length + 1];
createHeap(heap,source);
int N = heap.length-1;
while(N!=1){
exch(heap,1,N);
N--;
sink(heap,1,N);
}
System.arraycopy(heap,1,source,0,source.length);
}
/**
*
* @param heap
* @param target 目标节点位置
* @param range 截止到节点为止
*/
public static void sink(Comparable[] heap,int target,int range){
int max;
while(2*target<=range){
if(2*target+1<=range){
if(less(heap,2*target,2*target+1)){
max = 2*target+1;
}else{
max = 2*target;
}
}else{
max = 2*target;
}
if(!less(heap,target,max)){
break;
}
exch(heap,target,max);
target = max;
}
}
//判断索引处 i处的元素是否小于j处的元素
public static boolean less(Comparable[] heap,int i,int j){
return heap[i].compareTo(heap[j]) < 0;
}
//交换位置
public static void exch(Comparable[] heap,int i,int j){
Comparable temp = heap[i];
heap[i] = heap[j];
heap[j] = temp;
}
}
5.1.7 优先队列
普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。在某些情况下,我们可能需要找出 队列中的最大值或者最小值,例如使用一个队列保存计算机的任务,一般情况下计算机的任务都是有优先级的,我们需要在这些计算机的任务中找出优先级最高的任务先执行,执行完毕后就需要把这个任务从队列中移除。普通的 队列要完成这样的功能,需要每次遍历队列中的所有元素,比较并找出最大值,效率不是很高,这个时候,我们就可以使用一种特殊的队列来完成这种需求,优先队列。
优先队列按照其作用不同,可以分为以下两种:
最大优先队列:
可以获取并删除队列中最大的值
最小优先队列:
可以获取并删除队列中最小的值
5.1.7.1 最大优先队列
堆这种结构是可以方便的删除最大的值,所以,接下来我们可以基于堆区实现最大优先队列
5.1.7.2 最小优先队列
最小优先队列实现起来也比较简单,我们同样也可以基于堆来完成最小优先队列。
我们前面学习堆的时候,堆中存放数据元素的数组要满足都满足如下特性:
1.最大的元素放在数组的索引 1 处。
2.每个结点的数据总是大于等于它的两个子结点的数据。
其实我们之前实现的堆可以把它叫做最大堆,我们可以用相反的思想实现最小堆,让堆中存放数据元素的数组满足
如下特性:
1.最小的元素放在数组的索引 1 处。
2.每个结点的数据总是小于等于它的两个子结点的数据
package com.lans.binary;
/**
* @author: lans
* @date: 2025/3/1
* @name: 刘宇
*/
public class MinQueue <T extends Comparable<T>> {
private T[] items;
private int N;
public MinQueue(int capacity){
items = (T[])new Comparable[capacity];
N=0;
}
//true-> i<j
public boolean less(int i,int j){
return items[i].compareTo(items[j])<0;
}
public void exch(int i,int j){
T temp = items[i];
items[i] = items[j];
items[j] = temp;
}
public T delMin(){
//找到最小值
T min = items[1];
//交换根节点和N处节点的位置
exch(1,N);
items[N]=null;
N--;
sink(1);
return min;
}
public void insert(T t){
items[++N] = t;
swim(N);
}
//上浮算法
public void swim(int k){
while(k>1){
if(less(k,k/2)){
exch(k,k/2);
}
k=k/2;
}
}
//下沉算法
public void sink(int k){
//如果没有子节点就不在继续下沉
while(2*k<=N){
//找出节点中比较小的值
int min = 2*k;
if(2*k+1<=N&&less(2*k+1,2*k)){
min = 2*k+1;
}
if(less(k,min)){
break;
}
exch(k,min);
k=min;
}
}
public int size(){
return N;
}
public boolean isEmpty(){
return N==0;
}
public static void main(String[] args) {
String[] strings = {"s","t","r","x","n","h","p"};
MinQueue<String> minQueue = new MinQueue<>(15);
for(String s:strings){
minQueue.insert(s);
}
String remove;
while(!minQueue.isEmpty()){
remove = minQueue.delMin();
System.out.println(remove);
}
}
}
5.1.7.3 索引优先队列
通过索引来访问存在于队列中的对象;
实现思路:
1.以把 k 看做是items 数组的索引,把 t 元素放到 items 数组的索引 k 处,这样我们再根据 k 获取元素 t 时就很方便了,直接就可以拿到items[k]即可。
2.步骤一完成后的结果,虽然我们给每个元素关联了一个整数,并且可以使用这个整数快速的获取到该元素,但是,items 数组中的元素顺序是随机的,并不是堆有序的,所以,为了完成这个需求,我们可以增加一个数组 int[]pq,来 保存每个元素在 items 数组中的索引,pq 数组需要堆有序,也就是说,pq[1]对应的数据元素 items[pq[1]]要小于等于 pq[2]和 pq[3]对应的数据元素 items[pq[2]]和 items[pq[3]]。
3.通过步骤二的分析,我们可以发现,其实我们通过上浮和下沉做堆调整的时候,其实调整的是 pq 数组。如果需要对 items 中的元素进行修改,比如让 items[0]=“H”,那么很显然,我们需要对 pq 中的数据做堆调整,而且是调整
pq[9]中元素的位置。但现在就会遇到一个问题,我们修改的是 items 数组中 0 索引处的值,如何才能快速的知道需
要挑中 pq[9]中元素的位置呢?
最直观的想法就是遍历 pq 数组,拿出每一个元素和 0 做比较,如果当前元素是 0,那么调整该索引处的元素即可但是效率很低
package com.lans.binary;
/**
* @author: lans
* @date: 2025/3/1
* @name: 刘宇
*/
public class IndexMinQueue <T extends Comparable<T>>{
private T[] items;//存储元素的数组
private int[] pq;//保存每个元素在items数组中的索引,pq是堆有序的
private int[] qp;//记录pq的逆序,pq中的索引是qp中的元素 pq中的元素是qp索引
private int N;
public IndexMinQueue(int size){
items = (T[])new Comparable[size];
pq = new int[size+1];
qp = new int[size+1];
N=0;
for(int i=0;i<qp.length;i++){
qp[i]=-1;
}
}
public boolean isEmpty(){
return N==0;
}
public int size(){
return N;
}
public boolean less(int i, int j){
//通过pq找到items中对应的元素
return items[pq[i]].compareTo(items[pq[j]])<0;
}
public void exch(int i, int j){
int temp = pq[i];
pq[i] = pq[j];
pq[j] = temp;
qp[pq[i]] = i;
qp[pq[j]] = j;
}
public boolean contains(int k){
return qp[k]!=-1;
}
//获取最小索引
public int minIndex(){
return pq[1];
}
public void insert(int i,T t){
if(contains(i)){
throw new RuntimeException("该索引已存在");
}
N++;
items[i]=t;
pq[N]=i;
qp[i]=N;
//上浮
swim(N);
}
//删除
public int delMin(){
int minIndex = pq[1];
//与N位置进行交换
exch(1,N);
//删除qp中索引对应的pq[N]
qp[pq[N]]=-1;
//删除pq中索引N的值
items[minIndex]=null;
N--;
sink(1);
return minIndex;
}
public void delete(int i){
//找出在pq位置的索引
int k = pq[i];
exch(k,N);
//删除qp中的索引值
qp[pq[N]]=-1;
//删除pq对应的索引值
pq[N]=-1;
//删除item对应元素
items[i]=null;
N--;
sink(k);
swim(k);
}
public void sink(int k){
while(2*k<=N){
int min = 2*k;
if(2*k+1<=N&&less(2*k+1,2*k)){
min = 2*k+1;
}
if(less(k,min)){
break;
}
exch(k,min);
k=min;
}
}
public void swim(int k){
while(k>1){
if(less(k,k/2)){
exch(k,k/2);
}
k=k/2;
}
}
public static void main(String[] args) {
String[] strings = {"S","C","W","E","A","B","N","A"};
IndexMinQueue<String> queue = new IndexMinQueue<>(15);
for(int i=0;i<strings.length;i++){
queue.insert(i,strings[i]);
}
System.out.println(queue.size());
//获取最小值的索引
System.out.println(queue.minIndex());
int minIndex=-1;
while(!queue.isEmpty()){
minIndex = queue.delMin();
System.out.print(minIndex+",");
}
}
}
5.1.8 二叉树问题分析
1.二叉树需要加载到内存,节点少还好但节点多了就存在问题.
2.构建二叉树时,需要进行多次i/o操作节点海量,构建二叉树时,速度有影响
3.节点海量,二叉树很高大 会降低操作速度.
5.1.9 多叉树
5.1.10 B 树介绍
B 树通过重新组织节点,降低树的高度,并且减少 i/o 读写次数来提升效率
1、 文件系统及数据库系统的设计者利用了磁盘预读原理,将一个节点的大小设为等于一个页(页得大小通常为 4k)
2、 将树的度 M 设置为 1024,在 600 亿个元素中最多只需要 4 次 I/O 操作就可以读取到想要的元素, B 树(B+)广泛应用于文件存储系统以及数据库系统中这样每个节点只需要一次 I/O 就可以完全载入
5.1.11 2-3 树
5.1.11.1 2-3 树介绍
2-3树是最简单的 B 树结构, 具有如下特点
1、 2-3 树的所有叶子节点都在同一层.(只要是 B 树都满足这个条件)
2、 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点.
3、 有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点.
4、 2-3 树是由二节点和三节点构成的树
5.1.11.2 2-3 树应用场景
将数列{16, 24, 12, 32, 14, 26, 34, 10, 8, 28, 38, 20} 构建成 2-3 树,并保证数据插入的大小顺序。
规则
1、2-3 树的所有叶子节点都在同一层.(只要是 B 树都满足这个条件)
2、有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点.
3、有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点
4、对于三节点的子树的值大小仍然遵守(BST 二叉排序树)的规则
5.1.12 B 树、B+树、B*树
5.1.12.1 B 树介绍
B-tree 树即 B 树,B 即 Balanced,平衡的意思。有人把 B-tree 翻译成 B-树,容易让人产生 误解。会以为 B-树是一种树,而 B 树又是另一种树。实际上,B-tree 就是指的 B 树。2-3 树和 2-3-4 树,他们就是 B 树(英语:B-tree 也写成 B-树),这里我们再做一个说明,我们在学习 Mysql 时,经常听到说某种类型的索引是基于 B 树或者 B+树的,如图:
1、B 树的阶:节点的最多子节点个数。比如 2-3 树的阶是 3,2-3-4 树的阶是 4
2、B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点
3、关键字集合分布在整颗树中, 即叶子节点和非叶子节点都存放数据
4、搜索有可能在非叶子结点结束
5、其搜索性能等价于在关键字全集内做一次二分查找
5.1.12.2 B+树介绍
1、 B+树的搜索与 B 树也基本相同,区别是 B+树只有达到叶子结点才命中(B 树可以在非 叶子结点命中),其性能也等价于在关键字全集做一次二分查找
2、 所有关键字都出现在叶子结点的链表中(即数据只能在叶子节点【也叫稠密索引】),且 链表中的关键字(数据)恰好是有序的。
3、 不可能在非叶子结点命中
4、 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数 据的数据层
5、 更适合文件索引系统
6、 B 树和 B+树各有自己的应用场景,不能说 B+树完全比 B 树好,反之亦然.
5.1.12.3 B*树介绍
B*树是 B+树的变体,在 B+树的 非根和非叶子结点再增指向兄弟的指针
1、 B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为 2/3,而 B+树的块的最低使用率为的 1/2。
2、 从第 1 个特点我们可以看出,B*树分配新结点的概率比 B+树要低,空间使用率更高
5.1.13 树结构应用
5.1.13.1 赫夫曼树
5.1.13.2 赫夫曼树介绍
5.1.13.3 赫夫曼概念及理解
路径和路径长度
在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为 1,则从根结点到第 L 层结点的路径长度为 L-1。
结点的权及带权路径长度
若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为: 从根结点到该结点之间的路径长度与该结点的权的乘积。
树的带权路径长度
树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为 WPL。
权值越大的结点距离根结点越近的二叉树才是最优二叉树
wpl 最小的就是赫夫曼树
练习:
需求:
给定一个数列{13,7,8,29,6,1}要求转成一颗赫夫曼树
1、 从小到大进行排序,每个数据都是一个节点,每个结点可以看成是一颗简单的二叉树
2、 取出根结点最小的两颗二叉树
3、 组成一颗新的二叉树,该新的二叉树根结点权值是前面两颗二叉树节点权值之和。
4、 将这颗新的二叉树以根结点的权值大小再次排序,不断重复以上步骤,直到数列中所有的数据被处理,即可得到一颗赫夫曼树
package com.lans.HuffmanTree;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
/**
* @author: lans
* @date: 2025/3/3
* @name: 刘宇
*/
public class huffmanTree {
public static void main(String[] args) {
int[] arr = {13, 7, 8, 29, 6, 1};
Node root = huffmanTree.createHuffmanTree(arr);
}
//创建哈夫曼树 并且把创建好的树返回回来
public static Node createHuffmanTree(int[] array) {
ArrayList<Node> nodes = new ArrayList<>();
//1.把数据组每一个元素构建成节点
for (int value : array) {
//2.把构建好的每一个节点存放在集合中去
nodes.add(new Node(value));
}
while (nodes.size()>1) {
//3.从小到大进行排序操作
Collections.sort(nodes);
/**
* 1.排序
* 2.取出两个最小的节点组合成二叉树
* 3.新的二叉树的权值是前两颗二叉树权值之和
* 4.将这颗新的二叉树以根节点的权值大小再次排序
*/
// 取出权值最小的两颗二叉树
Node leftNode = nodes.get(0);
Node rightNode = nodes.get(1);
Node parentNode = new Node(leftNode.value + rightNode.value);
parentNode.left = leftNode;
parentNode.right = rightNode;
//删除已构建的节点 放入新的节点
nodes.remove(leftNode);
nodes.remove(rightNode);
nodes.add(parentNode);
}
return nodes.get(0);
}
}
5.1.13.4 赫夫曼编码
1、 赫夫曼编码是赫夫曼树在通讯中的经典应用案例
2、 赫夫曼编码广泛低用于数据文件压缩,它的压缩率在 20%~90%之间
5.1.13.5 赫夫曼编码原理
5.1.13.6 通讯中-----定长编码
5.1.13.7通讯中-----变长编码
例如:
字符串:
I'm hanguoqing. What's your name
g:1 q:1 i:1 W:1 s:1 y:1 r:1 e:1 I:1 m:2 h:2 u:2 o:2 a:3 n:3 :4
0= , 1=n, 11=a, 100=o, 101=u,111=h,1000=m,1001=I,1011=e …
按照上面编码二进制应该是如下:
100110000111
5.1.13.8 通讯中-----赫夫曼编码
I'm hanguoqing. What's your name
g:1 q:1 i:1 W:1 s:1 y:1 r:1 e:1 I:1 m:2 h:2 u:2 o:2 a:3 n:3 :4
按照上面字符出现的次数构建一颗赫夫曼树,次数作为权值
注意:
1、 根据赫夫曼树,规定前缀编码,向左的路径为 0,向右的路径为 1
2、 前缀编码:设计长短不等的编码,必须是任一字符的编码都不是另一个字符编码
的前缀,这种编码称为前缀编码
3、 如下,i:101 u:10010
5.1.13.9赫夫曼编码-数据压缩
给出一个文本,“Im hanguoqing. Whats your name”,根据我们讲的赫夫曼编码原理,对其进 行数据压缩
1.构建哈夫曼树
5.1.13.13 赫夫曼编码-数据解压
5.1.13.14 二叉排序树
需求分析:
假设给定一个数列 [36,65,18,7,60,89,43,57,96,52,74],能够有效的完成对数据的查询和添加。
您有什么样的思路吗
思路分析:
使用数组:
1、使用未排序数组可以直接将数组插入到数组尾端,速度快,但是查找慢
2、使用排序好数组,查询比较快,但是插入新数据时,需要整体移动后,插入有效的位
置,过程比较慢
使用链表:
链表特点是插入数据比较方便,但是查询数据比较慢
5.1.13.17 二叉排序树介绍
二叉排序树(Binary Sort Tree),又称二叉查找树(Binary Search Tree),亦称二叉搜索树。是数据结构中的一类。在一般情况下,查询效率比链表结构要高。
左小右大
如果有相同的值,可以将该节点放在左子节点和右子节点上
同一集数据对应的二叉排序树不唯一。但经过中序遍历得到的关键码序列都是一个递增序列。
删除:
其中二叉排序树是一个动态树表,其特点就是树的结构不是一次生成的,而是在查找过程中,如果关键字不在树表中,在把关键字插入到树表中。而对于删除操作,它分为三种情况
1、删除结点为叶子结点(左右子树均为 NULL),所以我们可以直接删除该结点,如下图所示:
2、删除结点只有左子树或者右子树,此时只需要让其左子树或者右子树直接替代删除结点的位置,称为删除结点的双亲的孩子,就可以了,如下图所示:
3、当要删除的那个结点,其左右子树都存在的情况下,则要从从左子树中找出一个最大的值那个结点来替换我们需要删除的结点,如下图所示:
package com.lans.tree;
/**
* @author: lans
* @date: 2025/3/5
* @name: 刘宇
*/
public class BinarySortTree {
public Node root;
/**
* 查找删除节点
*/
public Node searchDel(int val){
if(root == null){
return null;
}else{
return root.searchDelNode(val);
}
}
/**
* 查找删除节点父节点
*/
public Node searchParent(int val){
if(root == null){
return null;
}else{
return root.searchDelNodeParent(val);
}
}
/**
* 添加节点
*/
public void add(Node node){
if(root == null){
root = node;
}else{
root.add(node);
}
}
/**
* 中序遍历
*/
public void infixSelect(){
if(root != null){
root.infixSelect();
}else{
System.out.println("空");
}
}
/**
* 删除节点
异常!!!
*/
public void delNode(int val){
if(root == null){
return ;
}else{
//找到要删除的目标节点
Node target = searchDel(val);
if(target == null){
return;
}
//二叉排序树当前只存在一个节点时
if(root.left==null&&root.right==null){
root=null;
return;
}
//找到要删除元素的父节点元素
Node parentNode = searchParent(val);
//判断删除的节点是否是叶子节点
if(target.left==null&&target.right==null){
if(parentNode.left!=null && parentNode.left.value == val){
//删除节点时父节点的左节点
parentNode.left = null;
}else if(parentNode.right!=null && parentNode.right.value == val){
//删除节点时父节点的右节点
parentNode.right = null;
}
}else if(target.left!=null&&target.right!=null){
//删除的节点存在左右子树
int minVal = delTreeMin(target.right);
target.value = minVal;
}else{
//删除的节点只有一棵树 可能只有左子树 也可能只有右子树
if(target.left!=null){
if(parentNode!=null){
//删除的节点时parentNode左子节点
if(parentNode.left.value == val){
parentNode.left = target.left;
}else {
parentNode.right = target.left;
}
}else{
root = target.left;
}
}else{
// 要删除节点存在右子树
if(parentNode!=null){
if(parentNode.left.value == val){
//父节点的左节点
parentNode.left = target.right;
}else{
//父节点的右节点
parentNode.right = target.right;
}
}else{
root = target.right;
}
}
}
}
}
public int delTreeMin(Node node){
Node target = node;
while(target.left!=null){
target = target.left;
}
delNode(target.value);
return target.value;
}
public static void main(String[] args) {
int[] arr = {7,3,10,12,5,1,9,2};
BinarySortTree bst = new BinarySortTree();
for(int i=0;i<arr.length;i++){
bst.add(new Node(arr[i]));
}
// bst.infixSelect();
bst.delNode(3);
System.out.println("===============");
bst.infixSelect();
}
}
class Node{
int value;
Node left;
Node right;
public Node(int value){
this.value = value;
}
public Node(int value, Node left, Node right) {
this.value = value;
this.left = left;
this.right = right;
}
/**
* 中序遍历
*/
public void infixSelect(){
if(this.left!=null){
this.left.infixSelect();
}
System.out.println(this);
if(this.right!=null){
this.right.infixSelect();
}
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
/**
* 添加一个新的节点
* @param node
*/
public void add(Node node){
if(node == null){
return;
}
if(node.value<this.value){
//放入左子节点 如果不为空 需要和左子节点进行比较
if(this.left == null){
this.left = node;
}else{
this.left.add(node);
}
}else{
//放入右子节点
if(this.right == null){
this.right = node;
}else{
this.right.add(node);
}
}
}
/**
* 查找要删除的节点
* @param value
* @return
*/
public Node searchDelNode(int value){
if(this.value == value){
return this;
}else if(value<this.value){
//往左子节点查找
if(this.left == null){
return null;
}
return this.left.searchDelNode(value);
}else{
//往右子节点查找
if(this.right ==null){
return null ;
}
return this.right.searchDelNode(value);
}
}
/**
* 查找删除节点的父节点
*/
public Node searchDelNodeParent(int value){
if((this.left!=null&&this.left.value==value)||(this.right!=null&&this.right.value==value)){
return this;
}else{
if(value<this.value&&this.left ==null){
return this.left.searchDelNodeParent(value);
}else if(value>this.value&&this.right ==null){
return this.right.searchDelNodeParent(value);
}else{
return null;
}
}
}
}
5.2哈希表
5.2.1 哈希表基本介绍
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构.也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。
思路分析:
比如使用添加一个学生到校园系统中去,输入学生学号可以查找出该学生所有信息。
要求:
添加学生编号时按照从低到高的顺序
使用链表来实现哈希表,该链表不带表头
package com.lans.table;
/**
* @author: lans
* @date: 2025/3/6
* @name: 刘宇
*/
public class Student {
public int id;
public String name;
public Student next;
public Student(int id, String name) {
this.id = id;
this.name = name;
}
public Student(){}
}
package com.lans.table;
/**
* @author: lans
* @date: 2025/3/6
* @name: 刘宇
*/
public class StudentLinkedList {
//头指针
private Student head;
public void add(Student student){
if(head == null){
head = student;
return;
}
Student temp = head;
while(true){
if(temp.next==null){
break;
}
temp = temp.next;
}
temp.next = student;
}
public void list() {
if(head == null){
System.out.println("List is empty");
System.out.println();
return;
}
Student temp = head;
while(true){
System.out.println("学生编号名称"+temp.id+temp.name);
if(temp.next==null){
break;
}
temp = temp.next;
}
System.out.println();
}
public Student findByStudentID(int id){
if(head==null){
System.out.println("链表为空");
return null;
}
Student temp = head;
while(true){
if(temp.id==id){
break;
}
if(temp.next==null) {
temp = null;
break;
}
temp =temp.next;
}
return temp;
}
}
package com.lans.table;
import java.util.Scanner;
/**
* @author: lans
* @date: 2025/3/8
* @name: 刘宇
*/
public class HashTable {
private StudentLinkedList[] studentLinkedList;
private int size;
public HashTable(int size) {
this.size = size;
studentLinkedList = new StudentLinkedList[size];
for (int i = 0; i < size; i++) {
studentLinkedList[i] = new StudentLinkedList();
}
}
/**
* 往哈希表中添加学生信息
*
* @param student
*/
public void add(Student student) {
int hashValue = hashValue(student.id);
studentLinkedList[hashValue].add(student);
}
/**
* 获取哈希表的索引值
*
* @param id
* @return
*/
public int hashValue(int id) {
return id % size;
}
/**
* 查看所有信息
*/
public void list() {
for (int i = 0; i < size; i++) {
studentLinkedList[i].list();
}
}
public void findByStduentId(int id) {
int hashValue = hashValue(id);
Student byStudentId = studentLinkedList[hashValue].findByStudentID(id);
if (byStudentId != null) {
System.out.printf("查询到在索引位置是 %d 链表中找到了此学生,名称是 %s ", hashValue, byStudentId.name);
} else {
System.out.println("未找到该学生");
}
}
public static void main(String[] args) {
HashTable hashTable = new HashTable(10);
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("add 添加学员");
System.out.println("list 显示");
System.out.println("find 查找学员");
System.out.println("exit 退出操作");
String input = scanner.next();
switch (input) {
case "add":
System.out.println("请输入学生id");
int id = scanner.nextInt();
System.out.println("请输入学生名称");
String name = scanner.next();
Student student = new Student(id, name);
hashTable.add(student);
System.out.println("添加成功!");
System.out.println();
break;
case "list":
hashTable.list();
break;
case "find":
System.out.println("请输入学生id编号:");
int findId = scanner.nextInt();
hashTable.findByStduentId(findId);
break;
case "exit":
scanner.close();
System.exit(0);
break;
}
}
}
}
5.3 图
5.3.1 图基本介绍
我们生活中经常使用的地图,基本上是由城市以及连接城市的道路组成,如果我们把城市看做是一个一个的点,把前面我们学了线性表和树,线性表局限于一个直接前驱和一个直接后继的关系,树也只能有一 个直接前驱也就是父节点。当我们需要表示多对多的关系时, 这里我们就用到了图
5.3.2 图的常用概念
在计算机科学中,一个图就是一些顶点的集合,这些顶点通过一系列边结对(连接)。顶点用圆圈表示,边就是这些圆圈之间的连线。顶点之间通过边连接。
1. 自环:即一条连接一个顶点和其自身的边;
2. 平行边:连接同一对顶点的两条边;
图的分类:
按照连接两个顶点的边的不同,可以把图分为以下两种:
无向图:边仅仅连接两个顶点,没有其他含义;
有向图:边不仅连接两个顶点,并且具有方向;
5.3.3 无向图
相邻顶点: 当两个顶点通过一条边相连时,我们称这两个顶点是相邻的,并且称这条边依附于这两个顶点。
度: 某个顶点的度就是依附于该顶点的边的个数
子图: 是一幅图的所有边的子集(包含这些边依附的顶点)组成的图;
路径: 是由边顺序连接的一系列的顶点组成
环: 是一条至少含有一条边且终点和起点相同的路径
连通图:
如果图中任意一个顶点都存在一条路径到达另外一个顶点,那么这幅图就称之为连通图
连通子图:
一个非连通图由若干连通的部分组成,每一个连通的部分都可以称为该图的连通子图
5.3.4 为什么要使用图
也许你心里想着,有什么大不了的。
如果你有一个编程问题可以通过顶点和边表示出来,那么你就可以将你的问题用图画出来,然后使用著名的图算法(比如广度优先搜索 或者 深度优先搜索)来找到解决方案。
例如,假设你有一系列任务需要完成,但是有的任务必须等待其他任务完成后才可以开始。
你可以通过非循环有向图来建立模型:
每一个顶点代表一个任务。两个任务之间的边表示目的任务必须等到源任务完成后才可以开始。比如,在任务 B 和任务 D 都完成之前,任务 C 不可以开始。在任务 A 完成之前,任务B 和 D 都不能开始。
现在这个问题就通过图描述清楚了,你可以使用深度优先搜索算法来执行执行拓扑排序。
这样就可以将所有的任务排入最优的执行顺序,保证等待任务完成的时间最小化。(这里可能的顺序之一是:A, B, D, E, C, F, G, H, I, J, K)
5.3.5 图的表示方式
图就是一堆顶点和边对象而已,但是怎么在代码中来描述呢?
图的表示方式有两种:二维数组表示(邻接矩阵);链表表示(邻接列表)
5.3.6 邻接列表
1.使用一个大小为 V 的数组 Queue[V] adj,把索引看做是顶点;
2.每个索引处 adj[v]存储了一个队列,该队列中存储的是所有与该顶点相邻的其他顶点
5.3.7 邻接矩阵
1. 使用一个 V*V 的二维数组 int[V][V] adj,把索引的值看做是顶点;
2. 如果顶点 v 和顶点 w 相连,我们只需要将 adj[v][w]和 adj[w][v]的值设置为 1,否则设置为 0 即可。
往这个图中添加顶点的成本非常昂贵,因为新的矩阵结果必须重新按照新的行/列创建,然后将已有的数据复制到新的矩阵中。
所以使用哪一个呢?大多数时候,选择邻接列表是正确的。下面是两种实现方法更详细的比较。
5.3.8 图的设计
package com.lans.graph;
import com.lans.linear.Queue;
import java.util.LinkedList;
/**
* @author: lans
* @date: 2025/3/8
* @name: 刘宇
* 使用邻接链表实现无向图
*/
public class Graph {
//顶点数量
private int V;
//边的数量
private int E;
//邻接表
private Queue<Integer>[] adj;
public Graph(int V) {
this.V = V;
this.E = 0;
this.adj = new Queue[V];
for(int i = 0;i<adj.length;i++){
adj[i] = new Queue<Integer>();
}
}
//获取顶点数量
public int V(){
return this.V;
}
//获取边的数量
public int E(){
return this.E;
}
//添加边
public void addEdge(int v,int w){
adj[v].enqueue(w);
adj[w].enqueue(v);
E++;
}
//返回V顶点链接的所有的顶点链表
public Queue<Integer>adj(int v){
return adj[v];
}
}
5.3.9 图的搜索
5.3.9.1 图遍历介绍
一个图有那么多个结点,如何遍历这些结点,需要特定策略。
一般有两种访问策略: 深度优先遍历,广度优先遍历
5.3.9.2 图的深度优先介绍
5.3.9.3 深度优先遍历基本思想
深度优先遍历(Depth-First-Search),从初始访问结点出发,初始访问结点可能有多个邻接结点,深度优先遍历的策略就是首先访问,第一个邻接结点,然后再以这个被访问的邻接结点作为初始结点,访问它的第一个邻接结点, 可以这样理解每次都在访问完当前结点后首先访问当前结点的第一个邻接结点。
所谓的深度优先搜索,指的是在搜索时,如果遇到一个结点既有子结点,又有兄弟结点,那么先找子结点,然后找兄弟结点。
深度优先搜索是一个递归的过程
5.3.9.4 深度优先遍历算法步骤
很明显,在由于边是没有方向的,所以,如果 4 和 5 顶点相连,那么 4 会出现在 5 的相邻链表中,5 也会出现在 4 的相邻链表中,那么为了不对顶点进行重复搜索,应该要有相应的标记来表示当前顶点有没有搜索过,可以使用一个布尔类型的数组 boolean[V] marked,索引代表顶点,值代表当前顶点是否已经搜索,
如果已经搜索,标记为 true,
如果没有搜索,标记为 false;
package com.lans.graph;
/**
* @author: lans
* @date: 2025/3/9
* @name: 刘宇
*/
public class DepthFirstSearch {
//用来描述顶点是否被遍历
private boolean[] marked;
//记录有多少个订单与s点相通
private int count;
public DepthFirstSearch(Graph G,int s) {
//初始化记录遍历的数组
marked = new boolean[G.V()];
dfs(G,s);
}
//深度优先遍历 找出G图中与v顶点所有相通的顶点
public void dfs(Graph G,int v){
marked[v] = true;
for(Integer w:G.adj(v)){
if(!marked[w]){
dfs(G,w);
}
}
count++;
}
//判断w是否与s相通
public boolean marked(int w){
return marked[w];
}
//与顶点s相通的所有顶点数量
public int count(){
return count;
}
}
package com.lans.graph;
/**
* @author: lans
* @date: 2025/3/9
* @name: 刘宇
*/
public class DepthFirstSearchTest {
public static void main(String[] args) {
Graph graph = new Graph(13);
graph.addEdge(0,5);
graph.addEdge(0,1);
graph.addEdge(0,2);
graph.addEdge(0,6);
graph.addEdge(5,3);
graph.addEdge(5,4);
graph.addEdge(3,4);
graph.addEdge(4,6);
graph.addEdge(7,8);
graph.addEdge(9,11);
graph.addEdge(9,10);
graph.addEdge(9,12);
graph.addEdge(11,12);
DepthFirstSearch depthFirstSearch = new DepthFirstSearch(graph,0);
int count = depthFirstSearch.count();
System.out.println("与0点相通的顶点的数量count="+count);
boolean marked1 = depthFirstSearch.marked(5);
boolean marked2 = depthFirstSearch.marked(7);
System.out.println("5是否与顶点相通"+marked1);
System.out.println("7是否与顶点相通"+marked2);
}
}
5.3.9.6 图的广度优先介绍
广度优先遍历基本思想所谓的深度优先搜索,指的是在搜索时,如果遇到一个结点既有子结点,又有兄弟结点,
那么先找兄弟结点,然后找子结点。
5.3.9.7 广度优先遍历算法实现
package com.lans.graph;
/**
* @author: lans
* @date: 2025/3/9
* @name: 刘宇
*/
public class DepthFirstSearch {
//用来描述顶点是否被遍历
private boolean[] marked;
//记录有多少个顶点与s点相通
private int count;
public DepthFirstSearch(Graph G,int s) {
//初始化记录遍历的数组
marked = new boolean[G.V()];
dfs(G,s);
}
//深度优先遍历 找出G图中与v顶点所有相通的顶点
public void dfs(Graph G,int v){
marked[v] = true;
for(Integer w:G.adj(v)){
if(!marked[w]){
dfs(G,w);
}
}
count++;
}
//判断w是否与s相通
public boolean marked(int w){
return marked[w];
}
//与顶点s相通的所有顶点数量
public int count(){
return count;
}
}
5.3.9.8 路径查找
在实际生活中,地图是我们经常使用的一种工具,通常我们会用它进行导航,输入一个出发城市,输入一个目的地城市,就可以把路线规划好,而在规划好的这个路线上,会路过很多中间的城市。这类问题翻译成专业问题就是:
从 s 顶点到 v 顶点是否存在一条路径?如果存在,请找出这条路径
我们实现路径查找,最基本的操作还是得遍历并搜索图,所以,我们的实现暂且基于深度优先搜索来完成。其搜索的过程是比较简单的。我们添加了 edgeTo[]整型数组,这个整型数组会记录从每个顶点回到起点 s 的路径。
如果我们把顶点设定为 0,那么它的搜索可以表示为下图
package com.lans.graph;
import com.lans.linear.Stack;
/**
* @author: lans
* @date: 2025/3/9
* @name: 刘宇
* 路径查找
*/
public class DepthFirstPaths {
private boolean[] marked;
//起始位置
private int s;
private int[] edgeTo;
public DepthFirstPaths(Graph G, int s) {
marked = new boolean[G.V()];
edgeTo = new int[G.V()];
this.s = s;
dfs(G,s);
}
private void dfs(Graph G, int v) {
marked[v] = true;
for(Integer w:G.adj(v)){
if(!marked[w]){
edgeTo[w]=v;
dfs(G,w);
}
}
}
public boolean hasPathTo(int v) {
return marked[v];
}
//路径
public Stack<Integer> pathTo(int v){
if(!hasPathTo(v)){
return null;
}
Stack<Integer> path = new Stack<>();
for(int x=v;x!=s;x=edgeTo[x]){
path.push(x);
}
path.push(s);
return path;
}
}
6.搜索算法
在 java 程序中一般常用到四种查找方式。顺序查找(线性),二分查找,插值查找,斐波那契查找
6.1 线性查找算法
线性查找又称顺序查找,是一种最简单的查找方法,它的基本思想是从第一个记录开始,逐个比较记录的关键字,直到和给定的 K 值相等,则查找成功;
6.2 二分查找算法
二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列
6.2.1 二分查找算法原理
6.2.2 二分查找代码
package com.lans.search;
/**
* @author: lans
* @date: 2025/3/9
* @name: 刘宇
*/
public class BinarySearch {
public static void main(String[] args) {
//二分查找算法的前提是一个有序的序列
int[] arr = {10,21,32,42,543,6575,4354546};
int search = search(arr,0,arr.length-1,543);
System.out.println("search = " + search);
}
//二分查找
public static int search(int[] arr,int left,int right,int target) {
if(left>right){
return -1;
}
//求出中间索引值
int mid = (left+right)/2;
//求出中间索引值对应的元素
int midVal = arr[mid];
if(target>midVal){
return search(arr,mid+1,right,target);
}else if(target<midVal){
return search(arr,left,mid-1,target);
}else{
return mid;
}
}
}
6.3 插值查找算法
插值查找,有序表的一种查找方式。插值查找是根据查找关键字与查找表中最大最小记录关键字比较后的查找方法。插值查找基于二分查找,将查找点的选择改进为自适应选择,提高查找效率。
6.3.1 插值查找算法原理
当我们从字典中查找 “algorithm” 这个单词的时候,我们肯定不会傻傻地像二分查找一样首 先从中间开始。相反,我们会从首字母为 a 的地方开始查找,然后根据第二个字母在字母表中的位置,找到相应的位置再继续查找,这样重复这个过程,直到我们查找到这个单词。
自适应公式:int mid = left+(right-left)*(findVal-arr[left])/(arr[right]-arr[left])
例如搜索0的时候就可以快速查找到.
package com.lans.search;
/**
* @author: lans
* @date: 2025/3/10
* @name: 刘宇
*/
public class InsertSearch {
public static void main(String[] args) {
//二分查找算法的前提是一个有序的序列
int[] arr = {10,21,32,42,543,6575,4354546};
int search = search(arr,0,arr.length-1,543);
System.out.println("search = " + search);
}
public static int search(int[] arr, int left, int right, int target) {
//防止数组越界
if(left>right || target<arr[0]||target>arr[arr.length-1]) {
return -1;
}
//求出mid的结果
int mid = left+(right-left)*(target-arr[left])/(arr[right]-arr[left]);
//求出mid对应的值
int midVal = arr[mid];
if(target>midVal) {
return search(arr,mid+1,right,target);
}else if(midVal>target) {
return search(arr,left,mid-1,target);
}else{
return mid;
}
}
}
6.4 黄金分割法算法(斐波那契算法)
6.4.1 介绍
在介绍斐波那契查找算法之前,先介绍一下很它紧密相连的一个概念——黄金分割。黄金 比例又称黄金分割,是指事物各部分间一定的数学比例关系,即将整体一分为二,较大部分与 较小部分之比等于整体与较大部分之比,其比值约为 1:0.618 或 1.618:1。因此被称为黄金分割。
斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89…….(从第三个数开始,后边每一个数都是前 两个数的和)。然后我们会发现,随着斐波那契数列的递增,前后两个数的比值会越来越接近 0.618,利用这个特性,我们就可以将黄金比例运用到查找技术中.
package com.lans.search;
import java.util.Arrays;
/**
* @author: lans
* @date: 2025/3/10
* @name: 刘宇
*/
public class FibonacciSearch {
private static int maxSize = 20;
public static void main(String[] args) {
int[] arr = {1,4,6,45,64,65,75,3443,23242,42344};
int i = fibSearch(arr,75);
System.out.println("i="+i);
}
//初始化斐波那契数列 mid = low+F(k-1)-1
public static int[] fib(){
int[] f = new int[maxSize];
f[0]=1;
f[1]=1;
for(int i=2;i<maxSize;i++){
f[i] = f[i-1]+f[i-2];
}
return f;
}
/**
* 求出数列中关键字的位置
*/
public static int fibSearch(int[] arr,int key){
int low = 0;
int hight = arr.length-1;
int mid;
//斐波那契分割的数值下标
int k = 0;
//获取斐波那契数列
int[] f = fib();
//获取k的下标值
while(hight>f[k]-1){
k++;
}
int[] temp = Arrays.copyOf(arr,f[k]);
for(int i=hight+1;i<temp.length;i++){
temp[i]=arr[hight];
}
//mid = low+F(k-1)-1
while(low<=hight){
mid = low+f[k-1]-1;
if(key<temp[mid]){
hight=mid-1;
k--;
}else if(key>temp[mid]){
low=mid+1;
k-=2;
}else{
if(mid<=hight){
return mid;
}else{
return hight;
}
}
}
return -1;
}
}
7.程序员必会算法
7.1 二分查找算法
前面有 不再重复
package com.lans.search;
/**
* @author: lans
* @date: 2025/3/9
* @name: 刘宇
*/
public class BinarySearch {
public static void main(String[] args) {
//二分查找算法的前提是一个有序的序列
int[] arr = {10,21,32,42,543,6575,4354546};
int search = search(arr,0,arr.length-1,543);
System.out.println("search = " + search);
}
//二分查找
public static int search(int[] arr,int left,int right,int target) {
if(left>right){
return -1;
}
//求出中间索引值
int mid = (left+right)/2;
//求出中间索引值对应的元素
int midVal = arr[mid];
if(target>midVal){
return search(arr,mid+1,right,target);
}else if(target<midVal){
return search(arr,left,mid-1,target);
}else{
return mid;
}
}
}
7.2 分治算法
分治算法的基本思想是将一个规模为 N 的问题分解为 K 个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。
7.2.2 分治算法步骤
(1)分解,将要解决的问题划分成若干规模较小的同类问题;
(2)求解,当子问题划分得足够小时,用较简单的方法解决;
(3)合并,按原问题的要求,将子问题的解逐层合并构成原问题的解
7.2.3 汉诺塔
汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着 64 片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
package com.lans.alg;
/**
* @author: lans
* @date: 2025/3/11
* @name: 刘宇
*/
public class Hanoitower {
public static void main(String[] args) {
hanoiTower(3,'A','B','C');
}
public static void hanoiTower(int num,char a,char b,char c) {
if(num==1){
System.out.println("第"+num+"个盘"+a+"-->"+c);
}else{
//如果num多个,需要先从a移动到b
hanoiTower(num-1,a,c,b);
System.out.println("第"+num+"个盘"+a+"-->"+c);
hanoiTower(num-1,b,a,c);
}
}
}
7.3 动态规划算法
7.3.1 动态规划算法介绍
动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得 到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不 是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复 计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。
7.3.2 动态规划算法实现
给定一个矩阵 m,从左上角开始每次只能向右走或者向下走,最后达到右下角的位置,路径中所有数字累加起来就是路径和,返回所有路径的最小路径和,如果给定的 m 如下,那么路径1,3,1,0,6,1,0 就是最小路径和,返回 12
package com.lans.alg;
/**
* @author: lans
* @date: 2025/3/11
* @name: 刘宇
*/
public class Dyanmic {
public static void main(String[] args) {
int[][] arry = {{1,3,5,9},{8,1,3,4},{5,0,6,1},{8,8,4,0}};
}
public static int dyanmic(int[][] arry) {
if(arry.length==0){
return 0;
}
//记录走过的路径位置
int[][]dp = new int[arry.length][arry[0].length];
dp[0][0] = arry[0][0];
for(int i=1;i<dp[0].length;i++){
//最小路径是左边的元素+自身的元素
dp[0][i]=dp[0][i-1]+arry[0][i];
}
for(int i=1;i<arry.length;i++){//遍历列
for(int j=0;j<dp[i].length;j++){
if(j==0){
dp[i][j]=dp[i-1][j]+arry[i][j];
}else if(dp[i-1][j]<dp[i][j-1]){
dp[i][j]=dp[i-1][j]+arry[i][j];
}else{
dp[i][j]=dp[i][j-1]+arry[i][j];
}
}
}
return dp[dp.length-1][dp[dp.length-1].length-1];
}
}
7.4 KMP 算法
7.4.1 素朴匹配算法介绍
一个字符串(模式串)在另一个字符串(主串)中的位置,称为字符串模式匹配。
在朴素的字符串模式匹配算法中,我们对主串 S 和模式串 T 分别设置指针 i 和 j,假设字符串下标从 0 开始,初始时 i 和 j 分别指向每个串的第 0 个位置。在第 n 趟匹配开始时,i 指向主串 S 中的第 n-1 个位置,j 指向模式串 T 的第 0 个位置,然后逐个向后比较。若 T 中的每一个字符都与 S 中的字符相等,则称匹配成功,否则,当遇到某个字符不相等时,i 重新指向 S 的第n 个位置,j 重新指向 T 的第 0 个位置,继续进行第 n+1 趟匹配。
7.4.2 素朴算法匹配规则
7.4.3 KMP 算法介绍
在进行字符串匹配时,KMP 算法与朴素算法最大的区别就在于 KMP 算法省去了主串与子串不必要的回溯,这也是 KMP 算法(在主串有较多重复时)更加高效的关键。
从上述例子可以看出 KMP 算法的第一个优点:避免了主串不必要的回溯。事实上,主串的任何回溯都是不必要的,所以在 KMP 算法中,任何情况下主串都不回溯。
主串有重复
这一次,子串自身出现了重复,即第 0-1 位的 ab 和第 3-4 位的 ab 相等,所以若继续按照例一的方式避免子串的回溯,就会出现下面的情况:
所以第四次匹配是必要的,不能跳过。但第二、三次匹配也是必要的吗?不难看出,答案是否定的。
由于在进行第一次匹配时,我们已经知道主串的 3-4 位为 ab,所以在第二次匹配中,我们完全可以直接让主串的第 5 位与子串的第 3 位进行比较,来避免子串不必要的回溯,减少比较次数,这也是 KMP 算法的第二个优点。
7.4.4 KMP 算法应用
我们不可能人工推算这些数值,因此我们需要一个数组来记录子串应回溯到的位置。
已知空格与 D 不匹配时,前面六个字符”ABCDAB”是匹配的。查表可知,最后一个匹配字符 B 对应的”部分匹配值”为 2,因此按照下面的公式算出向后移动的位数:
移动位数 = 已匹配的字符数 - 对应的部分匹配值
因为 6 - 2 等于 4,所以将搜索词向后移动 4 位。
匹配值算法:
部分匹配表:
“部分匹配值”就是”前缀”和”后缀”的最长的共有元素的长度。以”ABCDABD”为例,
-”A”的前缀和后缀都为空集,共有元素的长度为 0;
-”AB”的前缀为[A],后缀为[B],共有元素的长度为 0;
-”ABC”的前缀为[A,AB],后缀为[BC, C],共有元素的长度 0;
-”ABCD”的前缀为[A,AB, ABC],后缀为[BCD, CD, D],共有元素的长度为 0;
-”ABCDA”的前缀为[A,AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为”A”,长度为 1;
-”ABCDAB”的前缀为[A,AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B], 共有元素为”AB”, 长度为 2;
-”ABCDABD”的前缀为[A,AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为 0。
package com.lans.alg;
import java.util.Arrays;
/**
* @author: lans
* @date: 2025/3/11
* @name: 刘宇
*/
public class KMP {
public static void main(String[] args) {
int[] next = kmpNext("abcabx");
int i = kmpSearch("abcabcabx","abcabx",next);
System.out.println(i);
}
public static int kmpSearch(String str1, String str2, int[] next) {
for(int i=0,j=0;i<str1.length();i++){
while(j>0&&str1.charAt(i)!=str2.charAt(j)){
j = next[j-1];
}
if(str1.charAt(i)==str2.charAt(j)){
j++;
}
if(j==str2.length()){
return i-j+1;
}
}
return -1;
}
/**
*
* @param desc 子串
* @return 部分匹配值数组
*/
public static int[] kmpNext(String desc){
int[] arr = new int[desc.length()];
arr[0]=0;
for(int i=1,j=0;i<desc.length();i++){
while(j>0&&desc.charAt(i)!=desc.charAt(j)){
j = arr[j-1];
}
if(desc.charAt(i)==desc.charAt(j)){
j++;
}
arr[i]=j;
}
return arr;
}
}
7.5 普里姆算法
7.5.1 应用场景
7 个村庄(A,B,C,D,E,F,G),现在需要修路把 7 个村庄连通,各个村庄的距离用边线表示(权),比如 A-B 距离 5 公里,问:如何修路保证各个村庄都能连通,并且总的修建公路总里程最短?
最小生成树:
最小生成树(Minimum Cost Spanning Tree),简称 MST,给定一个带权的左向连接通图,如何选取一颗生成树,使树上所有边上权的总和为最小,这叫最小生成树,N 个顶点,一定有 N-1 条边,包含全部顶点,N-1 条边都在图中,如下图
最小生成树的算法主要是普里姆算法和克鲁斯卡尔算法
7.5.2 普里姆算法介绍
普利姆(Prim)算法求最小生成树,也就是在包含 n 个顶点的连通图中,找出只有(n-1)条边包含所 有 n 个顶点的连通子图,也就是所谓的极小连通子图
普里姆算法如下:
(1)设 G=(V,E)是连通网,T=(U,D)是最小生成树,V,U 是顶点集合,E,D 是边的集合
(2)若从顶点 u 开始构造最小生成树,则从集合 v 中取出顶点 u 放入集合 U 中,标记顶点 v 的 visited[u] = 1
(3)若集合 U 中顶点 ui 与集合 V-U 中的顶点 vj 之间存在边,则寻找这些边中权值最小的边,单不能构成回路,将顶点 vj 加入集合 U 中,将边(ui,vj)加入集合 D 中,标记 visited[vj]=1
(4)重复步骤 2,知道 U,V 相等,即所有顶点都被标记为访问过,此时 D 中有 n-1 条边
package com.lans.alg;
import java.util.Arrays;
/**
* @author: lans
* @date: 2025/3/11
* @name: 刘宇
*/
public class MinTree {
public static void main(String[] args) {
char[] data = {'A', 'B', 'C', 'D', 'E', 'F','G'};
int verxs = data.length;
int[][] weight = {
{10000,5,7,10000,10000,10000,2}, //A
{5,10000,10000,9,10000,10000,3}, //B
{7,10000,10000,10000,8,10000,10000}, //C
{10000,9,10000,10000,10000,4,10000}, //D
{10000,10000,8,10000,10000,5,4}, //E
{10000,10000,10000,4,5,10000,6}, //F
{2,3,10000,10000,4,6,10000}};//G
MGraph mGraph = new MGraph(verxs);
createGraph(mGraph,verxs,data,weight);
lineGraph(mGraph);
prim(mGraph,1);
}
/**
*
* @param graph 图
* @param v 从哪个顶点开始计算 0表示从A开始
*/
public static void prim(MGraph graph,int v){
//判断标记的节点是否被访问过 0/1 no/yes
int[] visited = new int[graph.verxs];
visited[v]=1;//表示已经访问过
int minWeight = 10000;
int h1=-1;
int h2=-1;
for(int k=0;k<graph.verxs-1;k++){//遍历每一条边 n-1
for(int i=0;i<graph.verxs;i++){//访问邻接数组中的每一个元素
for(int j=0;j<graph.verxs;j++){
if(visited[i]==1&&visited[j]==0&&graph.weight[i][j]<minWeight){
//说明i已经被找过 j没有找到 此时就去找i到j的距离最短 i到j最短距离记录下来
minWeight = graph.weight[i][j];
h1=i;
h2=j;
}
}
}
//找到最小的一条边
System.out.println("边"+graph.data[h1]+" , "+graph.data[h2]+" 权值: "+minWeight);
visited[h2]=1;
minWeight = 10000;
}
}
public static void createGraph(MGraph graph,int verxs,char[] data,int[][]weight) {
int i,j;
for(i=0;i<verxs;i++){
graph.data[i]=data[i];
for(j=0;j<verxs;j++){
graph.weight[i][j]=weight[i][j];
}
}
}
public static void lineGraph(MGraph graph){
for(int[] i:graph.weight){
System.out.println(Arrays.toString(i));
}
}
}
class MGraph{
//图的节点数
int verxs;
//存放节点数
char[] data;
//邻接矩阵,用来表示顶点和顶点之间边的距离
int[][] weight;
public MGraph(int verxs){
this.verxs = verxs;
data = new char[verxs];
weight = new int[verxs][verxs];
}
}
7.6 迪杰斯特拉算法
7.6.1 迪杰斯特拉算法介绍
迪杰斯特拉(Dijkstra)算法是 典型最短路径算法,用于计算一个结点到其他结点的最短路径。 它的主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。
在有向图 G=(V,E) 中,假设每条边 E[i] 的长度为 w[i],找到由顶点 V0 到其余各点的最短 值。
本节将对算法流程进行模拟,设置 Graph 为包含 7 个顶点和 9 条边的有向无环图,源点为 0,计算从源点 0 到剩余节点的最短路径,Graph 如下:
每个节点将维护 shortest 和 visited 两个数据结构,
shortest 存储 v0 到该节点的最短路径,visited存储 v0 到该节点的最短路径是否求出。S 为已求出最短路径的节点,T 为未求出最短路径的节点。源节点只允许将 S 中的节点作为中间节点来计算到达其它节点的最短路径,不允许将 T中的节点作为中间节点来计算到达其它节点的最短路径。随着 S 中节点的增加,源节点可达的节点才会增加。初始状态下,源节点只可达节点 1 和节点 3。
1、 将源节点(即节点 0)加入 S 中,对 shortest 和 visited 数组进行更新。
2、 S 中现有节点 0,源节点可达 T 中的节点 1 和节点 3,节点 0->节点 1 距离为 6,节点 0->节点 3 距离为 2,按距离从小到大排序,因此选择将节点 3 加入 S 中。更新源点将节点 3作为中间节点到达其它节点的距离。
3、 S 中现有节点 0 和节点 3,源节点可达 T 中的节点 1 和 4,节点 0->节点 1 距离为 6,节点 0->节点 4 距离为 7,按距离从小到大排序,因此选择将节点 1 加入 S 中。更新源点将节点 1 作为中间节点到达其它节点的距离。
4、 S 中现有节点 0、1、3,源节点可达 T 中的节点 2、4、5,0->2 距离为 11,0->4 距离为 7,0->5 距离为 9,按距离从小到大排序,因此选择将节点 4 加入 S 中。更新源点将节点 4 作为中间节点到达其它节点的距离。
5、 S 中现有节点 0、1、3、4,源节点可达 T 中的节点 2、5、6,0->2 距离为 11,0->5 距离 为 9,0->6 距离为 8,按距离从小到大排序,因此选择将节点 6 加入 S 中。更新源点将节点 6 作为中间节点到达其它节点的距离。
6、 S 中现有节点 0、1、3、4、6,源节点可达 T 中的节点 2、5,0->2 距离为 11,0->5 距离为 9,按距离从小到大排序,因此选择将节点 5 加入 S 中。更新源点将节点 5 作为中间节点到达其它节点的距离。
7、 T 中只剩下节点 2,0->2 距离为 11,将节点 2 加入 S 中。
8、 算法结束,源点到其它节点的最短路径都已依次求出。
package com.lans.alg;
/**
* @author: lans
* @date: 2025/3/12
* @name: 刘宇
*/
public class DijstraAlg {
public static void main(String[] args) {
//二维数组来描述顶点之间的距离
int vertex = 7;
int[][] matrix = new int[vertex][vertex];
for (int i = 0; i < vertex; i++) {
for (int j = 0; j < vertex; j++) {
matrix[i][j] = 10000;//默认10000
}
}
matrix[0][1] = 6;
matrix[1][2] = 5;
matrix[0][3] = 2;
matrix[3][1] = 7;
matrix[3][4] = 5;
matrix[1][5] = 3;
matrix[4][5] = 5;
matrix[4][6] = 1;
}
/**
*
* @param matrix 邻接矩阵数组
* @param source 起始定点
*/
public static void dijstra(int[][] matrix,int source){
//最短路径
int[] shorttest = new int[matrix.length];
//是否存在最短路径
int[] visited = new int[matrix.length];
//用来计算顶点到顶点路径
String[] path = new String[matrix.length];
for(int i=0;i<matrix.length;i++){
path[i] = new String(source+"---->"+i);
}
//自身到自身的最短距离为0
shorttest[source] = 0;
visited[source] = 1;
for(int i=1;i<matrix.length;i++){
int min = Integer.MAX_VALUE;//初始化
int index=-1;
for(int j=0;j<matrix.length;j++){
if(visited[j]==0 && matrix[source][j]<min){//没找过且
min = matrix[source][j];
index = j;
}
}
shorttest[index]=min;
visited[index]=1;
for(int m=0;m<matrix.length;m++){
if(visited[m]==0 &&matrix[source][index]+matrix[index][m]<matrix[source][m]){
matrix[source][m]=matrix[source][index]+matrix[index][m];
path[m]=path[index]+"--->"+m;
}
}
}
for(int i=0;i<matrix.length;i++){
if(i!=source){
if(shorttest[i]==10000){
System.out.println(source+" 到 "+i+" 不可直达");
}else{
System.out.println(source+" 到 "+i+" 最短路径为: "+path[i]+" 最短距离: "+shorttest[i=]);
}
}
}
}
}