数据结构:
# 数据结构包括:线性结构和非线性结构
# 线性结构:
1. 线性结构作为最常用的数据结构,其特点是数据元素之间存在一对一的线性关系
2. 线性结构有两种不同的存储结构,即顺序存储结构和链式存储结构。顺序存储的线性表称为线性表,顺序表中的存储元素是连续的
3. 链式存储的线性表称为链表,链表中的存储元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息
4. 线性结构常见的有:数组、队列、链表和栈
# 非线性结构:
非线性结构包括:二维数组、多维数组、广义表、树结构、图结构
稀疏数组:
# 稀疏数组:(sparsearray)(稀疏数组也是一个二维数组)
当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组。
稀疏数组的处理方法:
1. 记录数组一共有几行几列,有多少个不同的值
2. 把具有不同的值的元素的行列及值记录在一个小规模的数组中,从而缩小程序的规模
# 二维数组转稀疏数组的思路:
1. 遍历 原始的二维数组,得到有效数据的个数 sum
2. 根据 sum 创建稀疏数组 sparseArr int[sum+1][3] (3 代表的是 行位置,列位置,值)
3. 将二维数组的有效数据存入到稀疏数组中
# 稀疏数组转二维数组的思路:
1. 先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组,比如:Arr = int[行数][列数]
2. 在读取稀疏数组后几行的数据,并赋值给二维数组即可.
# 应用实例:
1. 使用稀疏数组,来保留类似前面的二维数组(棋盘,地图等待)
2. 把稀疏数组存盘,并且可以重新恢复原来的二维数组数
### 代码实现:
public static void main(String[] args) {
// 创建一个原始的二维数组 11*11
// 0: 表示没有棋子,1: 表示 黑子,2: 表示 白子
int[][] chessArr1 = new int[11][11];
chessArr1[1][2] = 1;
chessArr1[2][4] = 2;
// 输出原始的二维数组
for (int[] ints : chessArr1) {
for (int anInt : ints) {
System.out.printf("%d\t", anInt);
}
System.out.println();
}
// 将二维数组 转 稀疏数组
// 1. 先遍历二维数组 得到非0数据的个数
int sum = 0;
for (int i = 0; i < 11; i++) {
for (int j = 0; j < 11; j++) {
if(chessArr1[i][j] != 0)
sum++;
}
}
System.out.println("sum = " + sum);
// 2. 创建对应的稀疏数组
int[][] sparseArr = new int[sum+1][3];
// 给稀疏数组赋值
sparseArr[0][0] = 11;
sparseArr[0][1] = 11;
sparseArr[0][2] = sum;
// 遍历二维数组,将非0的值存放在稀疏数组中
int count = 0;
for (int i = 0; i < 11; i++) {
for (int j = 0; j < 11; j++) {
if(chessArr1[i][j] != 0){
count++;
sparseArr[count][0] = i;
sparseArr[count][1] = j;
sparseArr[count][2] = chessArr1[i][j];
}
}
}
// 输出稀疏数组的输出
System.out.println();
System.out.println("得到的稀疏数组如下~~~~");
for (int i = 0; i < sparseArr.length; i++) {
System.out.printf("%d\t%d\t%d\t\n", sparseArr[i][0], sparseArr[i][1], sparseArr[i][2]);
}
// 将稀疏数组 -》 原始的二维数组
// 1. 读取稀疏数组的第一行,根据第一行的数据,创建原始数组
int[][] chessArr2 = new int[sparseArr[0][0]][sparseArr[0][1]];
// 2. 读取稀疏数组,并赋值给原始数组数据
for (int i = 1; i < sparseArr.length; i++) {
chessArr2[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][2];
}
System.out.println("恢复后的二维数组~~~~");
// 输出 恢复后的二维数组
for (int[] ints : chessArr2) {
for (int anInt : ints) {
System.out.printf("%d\t", anInt);
}
System.out.println();
}
}
队列:
# 队列介绍
1. 队列是一个有序列表,可以用数组或是链表来实现
2. 遵循先入先出的原则。即:先存入队列的数据,要先取出。后存入的要后取出
###### 数组模拟队列:
# 代码实现:
public class ArrayQueueDemo {
public static void main(String[] args) {
// 创建一个队列
ArrayQueue arrayQueue = new ArrayQueue(3);
char key = ' ';// 接收用户输入
Scanner scanner = new Scanner(System.in);
boolean loop = true;
// 输出一个菜单
while (loop) {
System.out.println("s(show): 显示队列");
System.out.println("a(add): 添加数据到队列");
System.out.println("g(get): 从队列取出数据");
System.out.println("h(head): 查看队列头的数据");
System.out.println("e(exit): 推出程序");
key = scanner.next().charAt(0);// 接收一个字符
switch (key) {
case 's':// 查看队列所有数据
arrayQueue.showQueue();
break;
case 'a':// 添加数据
System.out.println("输入一个数");
int value = scanner.nextInt();
arrayQueue.addQueue(value);
break;
case 'g':// 取出数据
try{
int res = arrayQueue.getQueue();
System.out.printf("取出的数据是 %d\n", res);
}catch (Exception e){
System.out.println(e.getMessage());
}
break;
case 'h':// 查看头部数据
try{
int res = arrayQueue.headQueue();
System.out.printf("取出的数据是 %d\n", res);
}catch (Exception e){
System.out.println(e.getMessage());
}
break;
case 'e':// 退出程序
scanner.close();
loop = false;
break;
default:
break;
}
}
System.out.println("程序退出~~~~");
}
}
class ArrayQueue{
private int maxSize;// 表示数组的最大容量
private int front;// 队列头
private int rear;// 队列尾
private int[] arr;// 该数组用于存放数据,模拟队列
// 创建队列的构造器
public ArrayQueue(int arrMaxSize){
maxSize = arrMaxSize;
front = -1;// 指向队列头部,分析出front是指向队列头的前一个位置
rear = -1;// 指向队列尾部,指向队列尾的数据(即: 队列最后一个数据)
arr = new int[arrMaxSize];
}
// 判断队列是否满
public boolean isFull(){
return rear == maxSize - 1;
}
// 判断队列是否为空
public boolean isEmpty(){
return rear == front;
}
// 添加数据到队列
public void addQueue(int n){
// 判断队列是否满
if(isFull()){
System.out.println("队列满,不能加入数据");
return;
}
rear++;// 让rear后移
arr[rear] = n;
}
// 获取队列的数据,出队列
public int getQueue(){
// 判断队列是否空
if(isEmpty()){
throw new RuntimeException("队列空,不能取数据");
}
front++;
return arr[front];
}
// 显示队列的所有数据
public void showQueue(){
// 遍历
if(isEmpty()){
System.out.println("队列空,无数据");
return;
}
for (int i = 0; i < arr.length ; i++) {
System.out.printf("arr[%d]=%d\n", i, arr[i]);
}
}
// 显示队列的头数据, 注意:不是取数据
public int headQueue(){
// 判断
if(isEmpty()){
throw new RuntimeException("队列为空,没有数据");
}
return arr[front + 1];
}
}
###### 数组模拟环形队列:
1. front变量的含义做一个调整:front指向队列的第一个元素,front的初始值=0
2. rear变量的含义做一个调整:rear指向队列的最后一个元素的后一个位置,空出一个空间作为约定,rear的初始值=0
3. 当队列满时,条件是 (rear+1)%maxSize==front
4. 当队列为空时,条件是 rear==front
5. 队列中有效的数据的个数: (rear+maxSize-front)%maxSize
6. 在原来 数组模拟队列的代码 基础上,改造为环形队列.
# 代码实现:
public class CircleArrayQueueDemo {
public static void main(String[] args) {
// 创建一个队列:这里设置4,其队列的有效数据最大为3个
CircleArray arrayQueue = new CircleArray(4);
char key = ' ';// 接收用户输入
Scanner scanner = new Scanner(System.in);
boolean loop = true;
// 输出一个菜单
while (loop) {
System.out.println("s(show): 显示队列");
System.out.println("a(add): 添加数据到队列");
System.out.println("g(get): 从队列取出数据");
System.out.println("h(head): 查看队列头的数据");
System.out.println("e(exit): 推出程序");
key = scanner.next().charAt(0);// 接收一个字符
switch (key) {
case 's':// 查看队列所有数据
arrayQueue.showQueue();
break;
case 'a':// 添加数据
System.out.println("输入一个数");
int value = scanner.nextInt();
arrayQueue.addQueue(value);
break;
case 'g':// 取出数据
try{
int res = arrayQueue.getQueue();
System.out.printf("取出的数据是 %d\n", res);
}catch (Exception e){
System.out.println(e.getMessage());
}
break;
case 'h':// 查看头部数据
try{
int res = arrayQueue.headQueue();
System.out.printf("头部的数据是 %d\n", res);
}catch (Exception e){
System.out.println(e.getMessage());
}
break;
case 'e':// 退出程序
scanner.close();
loop = false;
break;
default:
break;
}
}
System.out.println("程序退出~~~~");
}
}
class CircleArray{
private int maxSize;// 表示数组的最大容量
private int front;// 队列头
private int rear;// 队列尾
private int[] arr;// 该数组用于存放数据,模拟队列
// 创建队列的构造器
public CircleArray(int arrMaxSize){
maxSize = arrMaxSize;
arr = new int[arrMaxSize];
}
// 判断队列是否满
public boolean isFull(){
return (rear + 1) % maxSize == front;
}
// 判断队列是否为空
public boolean isEmpty(){
return rear == front;
}
// 添加数据到队列
public void addQueue(int n){
// 判断队列是否满
if(isFull()){
System.out.println("队列满,不能加入数据");
return;
}
// 直接将数据加入
arr[rear] = n;
// 将rear后移,这里必须考虑取模
rear = (rear + 1) % maxSize;
}
// 获取队列的数据,出队列
public int getQueue(){
// 判断队列是否空
if(isEmpty()){
throw new RuntimeException("队列空,不能取数据");
}
// 这里需要分析出 front 是指向队列的第一个元素
// 1. 先把front对应的值保留到一个临时变量
// 2. 将front后移,考虑取模
// 3. 将临时保存的变量返回
int value = arr[front];
front = (front + 1) % maxSize;
return value;
}
// 显示队列的所有数据
public void showQueue(){
// 遍历
if(isEmpty()){
System.out.println("队列空,无数据");
return;
}
for (int i = front; i < front + size() ; i++) {
System.out.printf("arr[%d]=%d\n", i % maxSize, arr[i % maxSize]);
}
}
// 显示当前队列有效数据的个数
public int size(){
return (rear + maxSize - front) % maxSize;
}
// 显示队列的头数据, 注意:不是取数据
public int headQueue(){
// 判断
if(isEmpty()){
throw new RuntimeException("队列为空,没有数据");
}
return arr[front];
}
}
链表:
# 链表:(Linked List) 介绍
1. 链表是一个有序的列表
2. 链表不同于数组,链表是以结点的形式存储,在物理空间上不一定连续
3. 链表的每个结点的内部包含data(数据)域,next(指针)域,指针域指向下一个结点的位置
4. 链表的头结点不存储数据,只是为了指向链表的开头
########## 单链表代码实现:(包括: 添加/删除/修改/查询)
# 小结:
1. 链表是以节点的方式来存储,是链式存储
2. 每个节点包含 data 域,next域:指向下一个节点
3. 链表的各个节点不一定是连续存储
4. 链表分带头节点链表和没有头节点的链表,根据实际的需求来确定.
### 由单链表的增加删除可以看出,链表的想要对指定索引进行操作(增加,删除),的时候必须获取该索引的前一个元素。记住这句话,对链表算法题很有用。
public class SingleLinkedListDemo {
public static void main(String[] args) {
// 进行测试
// 先创建节点
HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
HeroNode hero4 = new HeroNode(4, "林冲", "豹子头");
HeroNode hero5 = new HeroNode(4, "无用", "大聪明");
// 创建单向链表
SingleLinkedList singleLinkedList = new SingleLinkedList();
// 第一种方式加入
// singleLinkedList.add(hero1);
// singleLinkedList.add(hero4);
// singleLinkedList.add(hero2);
// singleLinkedList.add(hero3);
// 第二种方式加入
singleLinkedList.addByOrder(hero1);
singleLinkedList.addByOrder(hero4);
singleLinkedList.addByOrder(hero2);
singleLinkedList.addByOrder(hero3);
// singleLinkedList.addByOrder(hero3);
singleLinkedList.update(hero5);
// 显示数据
singleLinkedList.list();
System.out.println();
// 删除操作
singleLinkedList.delete(4);
singleLinkedList.list();
}
}
// 定义 SingleLinkedList
class SingleLinkedList{
// 先初始化一个头节点,头节点不存放具体的数据
HeroNode head = new HeroNode(0, "", "");
// # 添加节点到单向链表
// 思路: 当不考虑编号顺序时
// 1. 找到当前链表的最后节点
// 2. 将最后这个节点的next 指向新的节点
public void add(HeroNode heroNode){
// 因为head节点不能动,所以需要一个临时 temp
HeroNode temp = head;
// 遍历链表,找到最后
while (true) {
// 找到链表的最后
if(temp.next == null){
break;
}
// 如果没有找到最后,将temp后移
temp = temp.next;
}
// 将最后这个节点的 next 指向新的节点
temp.next = heroNode;
}
// # 第二种方式添加英雄,根据排名将英雄插入到指定位置
// 如果排名存在,则添加失败
public void addByOrder(HeroNode heroNode){
// 因为head节点不能动,所以需要一个临时 temp
HeroNode temp = head;
boolean flag = false;// 判断编号是否存在
while (true) {
if(temp.next == null){
break;
}
if(temp.next.no > heroNode.no){// 位置找到,就在temp后面插入
break;
}
if(temp.next.no == heroNode.no){// 说明编号存在
flag = true;
break;
}
temp = temp.next;// 后移,遍历当前链表
}
// 判断 flag 的值
if(flag){
System.out.printf("准备插入的英雄的编号 %d 已经存在,不能加入了\n", heroNode.no);
}else {
// 插入到链表中,temp的后面
heroNode.next = temp.next;
temp.next = heroNode;
}
}
// # 修改节点的信息,根据no编号来修改,即no编号不能改
public void update(HeroNode heroNode){
// 判断是否为空
if(head.next == null){
System.out.println("链表为空");
return;
}
HeroNode temp = head;
boolean flag = false;
while (true) {
if(temp == null){
break;
}
if(temp.no == heroNode.no){
flag = true;
break;
}
temp = temp.next;
}
// 根据flag判断是否找到要修改的节点
if(flag){
temp.name = heroNode.name;
temp.nickName = heroNode.nickName;
}else {
System.out.printf("没有找到编号 %d 的节点,不能修改\n", heroNode.no);
}
}
// # 删除节点信息
public void delete(int no){
// 判断是否为空
if(head.next == null){
System.out.println("链表为空");
return;
}
HeroNode temp = head;
boolean flag = false;
while (true) {
if(temp.next == null){
break;
}
if(temp.next.no == no){
flag = true;
break;
}
temp = temp.next;
}
// 根据flag判断是否找到要删除的节点
if(flag){
temp.next = temp.next.next;
}else {
System.out.printf("没有找到编号 %d 的节点,不能删除\n", no);
}
}
// # 显示链表[遍历]
public void list(){
// 判断链表是否为空
if(head.next == null){
System.out.println("链表为空");
return;
}
// 因为head节点不能动,所以需要一个临时 temp
HeroNode temp = head.next;
while (true) {
// 判断是否到链表最后
if(temp == null){
break;
}
// 输出节点的信息
System.out.println(temp);
// 将temp后移
temp = temp.next;
}
}
}
// 定义 HeroNode, 每个HeroNode对象就是一个节点
class HeroNode{
public int no;
public String name;
public String nickName;
public HeroNode next;// 指向下一个节点
// 构造器
public HeroNode(int no, String name, String nickName){
this.no = no;
this.name = name;
this.nickName = nickName;
}
// 重写toString方法
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickName='" + nickName + '\'' +
'}';
}
}
########### 双向链表:
双链表是链表的一种,由节点组成,每个数据结点中都有两个指针,分别指向直接后继和直接前驱。
# 单链表的缺陷
单链表只能从头结点开始访问链表中的数据元素,如果需要逆序访问单链表中的数据元素将极其低效。
# 双向链表代码实现:(添加/修改/删除/查询)
public class DoubleLinkedListDemo {
public static void main(String[] args) {
// 双向链表的测试
System.out.println("双向链表的测试~~~~~");
HeroNode2 hero1 = new HeroNode2(1, "宋江", "及时雨");
HeroNode2 hero2 = new HeroNode2(2, "卢俊义", "玉麒麟");
HeroNode2 hero3 = new HeroNode2(3, "吴用", "智多星");
HeroNode2 hero4 = new HeroNode2(4, "林冲", "豹子头");
HeroNode2 hero5 = new HeroNode2(4, "猪八戒", "天蓬元帅");
// 创建一个双向链表
DoubleLinkedList doubleLinkedList = new DoubleLinkedList();
doubleLinkedList.addByOrder(hero1);
doubleLinkedList.addByOrder(hero2);
doubleLinkedList.addByOrder(hero3);
doubleLinkedList.addByOrder(hero4);
doubleLinkedList.addByOrder(hero5);
doubleLinkedList.addByOrder(hero1);
doubleLinkedList.list();
// // 修改
// HeroNode2 newHero = new HeroNode2(4, "孙悟空", "齐天大圣");
// doubleLinkedList.update(newHero);
// System.out.println("修改后的链表情况~~");
// doubleLinkedList.list();
//
// // 删除
// doubleLinkedList.delete(4);
// System.out.println("删除后链表的情况~~");
// doubleLinkedList.list();
}
}
// 定义 DoubleLinkedList
class DoubleLinkedList{
// 先初始化一个头节点,头节点不存放具体的数据
HeroNode2 head = new HeroNode2(0, "", "");
// 返回头节点
public HeroNode2 getHead(){
return head;
}
// 添加节点到单向链表
// 思路: 当不考虑编号顺序时
// 1. 找到当前链表的最后节点
// 2. 将最后这个节点的next 指向新的节点
public void add(HeroNode2 heroNode){
// 因为head节点不能动,所以需要一个临时 temp
HeroNode2 temp = head;
// 遍历链表,找到最后
while (true) {
// 找到链表的最后
if(temp.next == null){
break;
}
// 如果没有找到最后,将temp后移
temp = temp.next;
}
// 将最后这个节点的 next 指向新的节点
// 形成一个双向链表
temp.next = heroNode;
heroNode.pre = temp;
}
// 第二种方式添加英雄,根据排名将英雄插入到指定位置
// 如果排名存在,则添加失败
public void addByOrder(HeroNode2 heroNode){
// 因为head节点不能动,所以需要一个临时 temp
HeroNode2 temp = head;
boolean flag = false;// 判断编号是否存在
while (true) {
if(temp.next == null){
break;
}
if(temp.next.no > heroNode.no){// 位置找到,就在temp后面插入
break;
}
if(temp.next.no == heroNode.no){// 说明编号存在
flag = true;
break;
}
temp = temp.next;// 后移,遍历当前链表
}
// 判断 flag 的值
if(flag){
System.out.printf("准备插入的英雄的编号 %d 已经存在,不能加入了\n", heroNode.no);
}else {
// temp.next 存起来
HeroNode2 value = temp.next;
// 插入到链表中,temp的后面
temp.next = heroNode;
heroNode.pre = temp;
if(value!= null){
heroNode.next = value;
value.pre = heroNode;
}
}
}
// 修改节点的信息,根据no编号来修改,即no编号不能改
public void update(HeroNode2 heroNode){
// 判断是否为空
if(head.next == null){
System.out.println("链表为空");
return;
}
HeroNode2 temp = head;
boolean flag = false;
while (true) {
if(temp == null){
break;
}
if(temp.no == heroNode.no){
flag = true;
break;
}
temp = temp.next;
}
// 根据flag判断是否找到要修改的节点
if(flag){
temp.name = heroNode.name;
temp.nickName = heroNode.nickName;
}else {
System.out.printf("没有找到编号 %d 的节点,不能修改\n", heroNode.no);
}
}
// 从双向链表中删除一个节点
// 1. 对于双向链表,我们可以直接找到要删除的这个节点
// 2. 找到后,自我删除即可
public void delete(int no){
// 判断是否为空
if(head.next == null){
System.out.println("链表为空");
return;
}
HeroNode2 temp = head.next;
boolean flag = false;
while (true) {
if(temp == null){
break;
}
if(temp.no == no){
flag = true;
break;
}
temp = temp.next;
}
// 根据flag判断是否找到要删除的节点
if(flag){
temp.pre.next = temp.next;
if(temp.next != null){
temp.next.pre = temp.pre;
}
}else {
System.out.printf("没有找到编号 %d 的节点,不能删除\n", no);
}
}
// 显示链表[遍历]
public void list(){
// 判断链表是否为空
if(head.next == null){
System.out.println("链表为空");
return;
}
// 因为head节点不能动,所以需要一个临时 temp
HeroNode2 temp = head.next;
while (true) {
// 判断是否到链表最后
if(temp == null){
break;
}
// 输出节点的信息
System.out.println(temp);
// 将temp后移
temp = temp.next;
}
}
}
// 定义 HeroNode, 每个HeroNode对象就是一个节点
class HeroNode2{
public int no;
public String name;
public String nickName;
public HeroNode2 pre;// 指向上一个节点
public HeroNode2 next;// 指向下一个节点
// 构造器
public HeroNode2(int no, String name, String nickName){
this.no = no;
this.name = name;
this.nickName = nickName;
}
// 重写toString方法
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickName='" + nickName + '\'' +
'}';
}
}
##### 单向环形链表
如果把单链表的最后一个节点的指针指向链表头部,而不是指向NULL,那么就构成了一个单向循环链表,通俗讲就是把尾节点的下一跳指向头结点。
# 为什么要使用单向循环链表?
在单向链表中,头指针是相当重要的,因为单向链表的操作都需要头指针,所以如果头指针丢失或者破坏,那么整个链表都会遗失,并且浪费链表内存空间,因此我们引入了单向循环链表这种数据结构。
##### (约瑟夫环问题)可以利用 数组/单向环形链表(不带头节点) 来完成
### 思路分析:
根据用户的输入,生成一个出圈的顺序
n = 5,即五个人
k = 1,从第几个开始
m = 2,间隔多少
1. 创建一个辅助指针(变量)helper,事先应该指向环形链表的最后一个节点
2. 先让first和helper移动k-1次,定位至开始出圈点
3. 让first和helper指针同时移动m-1次
4. 这时可以将first指向的节点出圈
first = first.next
helper.next = first
原来的first指向的节点就没有任何引用,会被回收
### 代码实现:
public class CircularSingleLinkedListDemo {
public static void main(String[] args) {
CircularSingleLinked circularSingleLinked = new CircularSingleLinked();
circularSingleLinked.addBoy(5);
circularSingleLinked.showBoy();
System.out.println();
circularSingleLinked.countBoy(1, 2, 5);
}
}
// 创建一个环形的单向链表
class CircularSingleLinked{
// 创建一个first节点,当前没有编号
private Boy first = new Boy(-1);
// 添加小孩节点,构建成一个环形的链表
public void addBoy(int nums){
// nums 做一个数据校验
if(nums < 1){
System.out.println("nums的值不正确");
return;
}
Boy curBoy = null;// 临时变量
// 使用for来创建环形链表
for (int i = 1; i <= nums; i++) {
// 根据编号,创建boy节点
Boy boy = new Boy(i);
// 如果是第一个节点
if(i == 1){
first = boy;
first.setNext(first);
curBoy = boy;
}else {
curBoy.setNext(boy);
boy.setNext(first);
curBoy = boy;
}
}
}
// 遍历当前的环形链表
public void showBoy(){
// 判断链表是否为空
if(first == null){
System.out.println("没有任何节点数据~~");
return;
}
Boy curBoy = first;// 临时变量
while(true) {
System.out.printf("小孩的编号 %d \n", curBoy.getNo());
if(curBoy.getNext() == first){// 说明遍历结束
break;
}
curBoy = curBoy.getNext();// curBoy后移
}
}
// 根据用户的输入,计算出数字出圈的顺序
/**
*
* @param startNo 表示从哪里开始
* @param countNum 表示间隔多少
* @param nums 表示最初的长度是多少
*/
public void countBoy(int startNo, int countNum, int nums){
// 先对数据进行校验
if(first == null || startNo < 1 || startNo > nums){
System.out.println("参数输入有误,请重新输入");
return;
}
// 创建辅助指针
Boy helper = first;
// helper 应指向环形链表的最后一个节点
while (true) {
if(helper.getNext() == first){
break;
}
helper = helper.getNext();
}
// 先让first和helper移动k-1次,定位至开始出圈点
for (int i = 0; i < startNo - 1; i++) {
first = first.getNext();
helper = helper.getNext();
}
// 循环计算出圈
while (true) {
if(helper == first){// 说明圈中只有一个节点
break;
}
// 让first和helper指针同时移动m-1次
for (int i = 0; i < countNum - 1; i++) {
first = first.getNext();
helper = helper.getNext();
}
// 这时可以将first指向的节点出圈
System.out.printf("小孩 %d 出圈\n", first.getNo());
first = first.getNext();
helper.setNext(first);
}
System.out.printf("最后留在圈中的小孩编号%d", first.getNo());
}
}
class Boy{
private int no;// 编号
private Boy next;// 指向下一个节点
public Boy(int no) {
this.no = no;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public Boy getNext() {
return next;
}
public void setNext(Boy next) {
this.next = next;
}
}
栈:
# 栈的介绍:(stack)
1. 栈是一个先入后出(FILO-First inLastOut)的有序列表
2. 栈是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)
3. 最先放入栈中的元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反
4.出栈(pop)、入栈(push)
# 栈的应用场景:
1. 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。
2. 处理递归调用:和子程序调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆栈中。
3. 表达式的转换[中缀表达式转后缀表达式]与求值(实际解决)。
4. 二叉树的遍历。
5. 图形的深度优先(depth-first)搜索法。
#### 使用数组模拟栈:
# 思路分析:
1. 使用数组模拟栈
2. 定义一个top来表示栈顶,初始化为-1
3. 入栈:top++,stack[top]=data
4. 出栈:int temp = top,top--,return stack[temp]
# 代码实现:(数组模拟栈)
public class ArrStackDemo {
public static void main(String[] args) {
// 测试
ArrayStack arrayStack = new ArrayStack(4);
String key = "";
boolean loop = true;// 控制是否退出菜单
Scanner scanner = new Scanner(System.in);
while (loop){
System.out.println("show: 显示栈数据");
System.out.println("exit: 退出程序");
System.out.println("push: 添加数据");
System.out.println("pop: 取出数据");
key = scanner.next();
switch (key){
case "show":
arrayStack.list();
break;
case "exit":
scanner.close();
loop = false;
break;
case "push":
System.out.println("请输入一个数字");
int value = scanner.nextInt();
arrayStack.push(value);
break;
case "pop":
try{
int res = arrayStack.pop();
System.out.printf("出栈的数据是 %d \n", res);
}catch (Exception e){
System.out.println(e.getMessage());
}
break;
default:
System.out.println("请输入正确的指令~");
break;
}
}
System.out.println("程序结束!");
}
}
// 定义一个 ArrayStack 表示栈
class ArrayStack{
private int maxSize;// 栈的大小
private int[] stack;// 数组模拟栈
private int top = -1;// top 表示栈顶,初始化为 -1
// 构造器
public ArrayStack(int maxSize){
this.maxSize = maxSize;
stack = new int[maxSize];
}
// 栈满
public boolean isFull(){
return top == maxSize - 1;
}
// 栈空
public boolean isEmpty(){
return top == -1;
}
// 入栈-push
public void push(int value){
if(isFull()){
System.out.println("栈满~");
return;
}
top++;
stack[top] = value;
}
// 出栈-pop
public int pop(){
if(isEmpty()) {
throw new RuntimeException("栈空!!!");
}
int temp = top;
top--;
return stack[temp];
}
// 遍历栈数据
public void list(){
if (isEmpty()){
System.out.println("栈空~");
return;
}
// 遍历栈需要从栈顶开始遍历
for (int i = top; i >= 0; i--) {
System.out.printf("stack[%d] = %d \n", i, stack[i]);
}
}
}
## 栈实现 综合计算器思路 分析:
1. 通过一个index值(索引),来遍历表达式
2. 如果是数字,就入 数栈
3. 如果是符号,就如下情况:
3.1. 如果发现当前的符号栈为空,直接入栈
3.2. 如果符号栈有操作符,就进行比较,如果当前的操作符的优先级小于或者等于栈中的操作符,就需要从数栈中pop出两个数,进行运算,将得到结果入数栈,然后将当前的操作符入符号栈。如果当前的操作符优先级大于栈中的操作符,就直接入符号栈.
4. 当表达式扫描完毕,就顺序的从数栈和符号栈中pop相应的数和符号,并运行.
5. 最后在数栈只有一个数字,就是表达式的结果.
# 代码实现:(中缀表达式实现)
// 计算器
public class Calculator {
public static void main(String[] args) {
// 表达式
String expression = "8*2*4-2";
// 创建两个栈:数栈、符号栈
ArrayStack2 numStack = new ArrayStack2(10);
ArrayStack2 operStack = new ArrayStack2(10);
// 定义需要的相关变量
int index = 0;// 用于扫描
int num1 = 0;
int num2 = 0;
int oper = 0;
int res = 0;
char ch = ' ';// 将每次扫描得到char保存到ch
String keepNum = "";// 用于拼接多位数
// 开始while循环的扫描expression
while (true) {
// 依次得到 expression 的每一个字符
ch = expression.substring(index, index + 1).charAt(0);
// 判断ch是什么,然后做相应的处理
if (operStack.isOper(ch)) {
if (!operStack.isEmpty()) {
if (operStack.priority(ch) <= operStack.priority(operStack.peek())) {
num1 = numStack.pop();
num2 = numStack.pop();
oper = operStack.pop();
res = numStack.cal(num1, num2, oper);
numStack.push(res);
operStack.push(ch);
} else {
operStack.push(ch);
}
}else {
operStack.push(ch);
}
} else {
// numStack.push(ch - 48);
// 处理多位数
keepNum += ch;
// 如果ch是expression最后一位,直接入栈
if(index == expression.length()-1){
numStack.push(Integer.valueOf(keepNum));
}else {
// 判断下一个字符是不是数组,是:继续扫描,否:入栈
if (operStack.isOper(expression.substring(index + 1, index + 2).charAt(0))) {
numStack.push(Integer.valueOf(keepNum));
keepNum = "";// 清空 keepNum
}
}
}
// 让 index + 1,并判断是否扫描到 expression 最后
index++;
if(index >= expression.length()){
break;
}
}
// 当表达式扫描完毕,顺序从数栈和符号栈pop出相应数和符号计算
while (true) {
if(operStack.isEmpty()){
break;
}
num1 = numStack.pop();
num2 = numStack.pop();
oper = operStack.pop();
res = numStack.cal(num1, num2, oper);
numStack.push(res);
}
System.out.printf("表达式 %s = %d", expression, numStack.pop());
}
}
// 定义一个 ArrayStack 表示栈
class ArrayStack2 {
private int maxSize;// 栈的大小
private int[] stack;// 数组模拟栈
private int top = -1;// top 表示栈顶,初始化为 -1
// 构造器
public ArrayStack2(int maxSize) {
this.maxSize = maxSize;
stack = new int[maxSize];
}
// 栈满
public boolean isFull() {
return top == maxSize - 1;
}
// 栈空
public boolean isEmpty() {
return top == -1;
}
// 入栈-push
public void push(int value) {
if (isFull()) {
System.out.println("栈满~");
return;
}
top++;
stack[top] = value;
}
// 出栈-pop
public int pop() {
if (isEmpty()) {
throw new RuntimeException("栈空!!!");
}
int temp = top;
top--;
return stack[temp];
}
// 返回当前栈顶的值
public int peek() {
return stack[top];
}
// 返回运算符的优先级,优先级自己来决定,使用数字表示
// 数字越大,优先级越高
public int priority(int oper) {
if (oper == '*' || oper == '/') {
return 1;
} else if (oper == '+' || oper == '-') {
return 0;
} else {
return -1;
}
}
// 判断是否为一个运算符
public boolean isOper(char val) {
return val == '+' || val == '-' || val == '*' || val == '/';
}
// 计算方法
public int cal(int num1, int num2, int oper) {
int res = 0;// res:用于存放计算的结果
switch (oper) {
case '+':
res = num1 + num2;
break;
case '-':
res = num2 - num1;
break;
case '*':
res = num1 * num2;
break;
case '/':
res = num2 / num1;
break;
default:
break;
}
return res;
}
}
栈的表达式:
### 栈的表达式:前缀、中缀、后缀表达式(逆波兰表达式)
# 前缀表达式: (波兰表达式)
前缀表达式是一种没有括号的算术表达式,与中缀表达式不同的是,其将运算符写在前面,操作数写在后面。为纪念其发明者波兰数学家Jan Lukasiewicz,前缀表达式也称为“波兰式”。例如,- 1 + 2 3,它等价于1-(2+3)。
# 中缀表达式:
中缀表达式是一个通用的算术或逻辑公式表示方法。(生活中常见的表达式)
# 后缀表达式: (逆波兰表达式)
逆波兰式(Reverse Polish notation,RPN,或逆波兰记法),也叫后缀表达式(将运算符写在操作数之后)
# 逆波兰计算器:
1.输入一个逆波兰表达式,使用栈计算其结果
2.支持小括号和多位整数,这里只对整数的计算
# 后缀表达式计算机求值思路:
从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符是,弹出栈顶的两个数,用运算符对他们(前两个元素)做出相应的计算,并将结果入栈,重复上述过程,直到表达式的最右端,最后运算得出的值即为表达式的结果
# 代码实现:
// 逆波兰计算器
public class PolandNotation {
public static void main(String[] args) {
// 先定义 逆波兰表达式
// (3+4)*5-6 => 3 4 + 5 * 6 -
// (3*5+5)/4 => 3 5 * 5 + 4 /
// 3+5+4/2 => 3 5 + 4 2 / +
String suffixExpression = "3 5 + 4 2 / +";
List<String> list = getListString(suffixExpression);
System.out.println("list => " + list);
int rusult = calculate(list);
System.out.println("计算的结果是 => " + rusult);
}
// 将逆波兰表达式,依次将数据和运算符存入 ArrayList 中
public static List<String> getListString(String suffixExpression){
// 将 suffixExpression 分割
String[] split = suffixExpression.split(" ");
List<String> list = new ArrayList<>();
for (String s : split) {
list.add(s);
}
return list;
}
// 完成对逆波兰表达式的运算
public static int calculate(List<String> ls){
// 创建一个栈
Stack<String> stack = new Stack<>();
for (String item : ls) {
// 利用正则表达式取出数
if(item.matches("\\d+")){// 匹配多位数
// 入栈
stack.push(item);
}else {
// pop出两个数并运算,再入栈
int num1 = Integer.valueOf(stack.pop());
int num2 = Integer.valueOf(stack.pop());
int res = 0;
switch (item) {
case "+":
res = num1 + num2;
break;
case "-":
res = num2 - num1;
break;
case "*":
res = num2 * num1;
break;
case "/":
res = num2 / num1;
break;
default:
throw new RuntimeException("运算符有误~");
}
stack.push(res + "");
}
}
return Integer.valueOf(stack.pop());
}
}
# 中缀表达式转后缀表达式:
# 思路分析:
前置转换: 将中缀表达式的字符拆分放入进List中.
1. 初始化两个栈:运算符栈s1和储存中间结果栈s2
2. 从左至右扫描中缀表达式
3. 遇到操作数时,放入s2
4. 遇到运算符时,比较其与s1栈顶运算符的优先级:
1. 如果s1为空,或栈顶运算符为"(",则直接将此运算符入栈
2. 否则,若优先级比栈顶的高,将运算符压入s1
3. 否则,将s1栈顶的运算符弹出并压入到s2中,再次判断4.1与s1中新的栈顶运算符相比较
5. 遇到括号时:
1. 如果是"(",则直接压入s1
2. 如果是")",则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃
6. 重复步骤 2至5,直到表达式的最右边
7. 将s1中剩余的运算符依次弹出并压入s2
8. 依次弹出s2中的元素并输出,结果的逆序即为 中缀表达式对应的后缀表达式.
9. 因为s2在整个转换过程中无pop操作,可以直接使用ArrayList替代使用.
### 中缀表达式转后缀表达式 功能代码实现:
// 中缀表达式转后缀表达式得到计算结果
public class PolandNotation {
public static void main(String[] args) {
String expression = "1+33+(2+5)*3";
List<String> infixExpressionList = toInfixExpressionList(expression);
System.out.println("中缀表达式:" + infixExpressionList);
List<String> suffixExpressionList = parseSuffixExpression(infixExpressionList);
System.out.println("后缀表达式:" + suffixExpressionList);
int rusult = calculate(suffixExpressionList);
System.out.println("计算的结果是 => " + rusult);
}
// 方法:将中缀表达式转成对应的List
public static List<String> toInfixExpressionList(String s){
// 定义一个List,存放中缀表达式对应的内容
List<String> ls = new ArrayList<>();
int i = 0;// 计数
String str;// 多位数的拼接
char c;// 每遍历到一个字符,就放入到 c
do {
// 如果 c 是一个非数字,需要加入到ls
if((c=s.charAt(i)) < 48 || (c=s.charAt(i)) > 57){
ls.add(c + "");
i++;// i 后移
}else {// 如果是一个数字,需要考虑多位数
str = "";
while (i < s.length() && (c=s.charAt(i)) >= 48 && (c=s.charAt(i)) <= 57){
str += c;
i++;
}
ls.add(str);
}
}while (i < s.length());
return ls;
}
// 方法:将得到的中缀表达式list => 后缀表达式对应的list
public static List<String> parseSuffixExpression(List<String> ls){
// 定义两个容器
Stack<String> s1 = new Stack<>();
List<String> s2 = new ArrayList<>();
// 遍历 ls
for (String item : ls) {
// 如果是一个数,加入s2
if(item.matches("\\d+")){
s2.add(item);
}else if(item.equals("(")){
s1.push(item);
}else if(item.equals(")")){
// 如果是 ),则依次弹出s1栈顶的运算符,放入s2,直到遇到 ( 为止,并删除
while (!s1.peek().equals("(")){
s2.add(s1.pop());
}
s1.pop();// 将 ( 消除
}else {
// 当item优先级小于等于s1栈顶运算符,将s1栈顶
// 运算符弹出并放入s2, 再次与s1栈顶新运算符比较
while (s1.size() != 0 && Operation.getPriority(s1.peek()) >= Operation.getPriority(item)){
s2.add(s1.pop());
}
// 将item放入s1
s1.push(item);
}
}
// 将s1剩余运算符依次弹出放入s2
while (s1.size() != 0) {
s2.add(s1.pop());
}
return s2;
}
// 完成对逆波兰表达式的运算
public static int calculate(List<String> ls){
// 创建一个栈
Stack<String> stack = new Stack<>();
for (String item : ls) {
// 利用正则表达式取出数
if(item.matches("\\d+")){// 匹配多位数
// 入栈
stack.push(item);
}else {
// pop出两个数并运算,再入栈
int num1 = Integer.valueOf(stack.pop());
int num2 = Integer.valueOf(stack.pop());
int res = 0;
switch (item) {
case "+":
res = num1 + num2;
break;
case "-":
res = num2 - num1;
break;
case "*":
res = num2 * num1;
break;
case "/":
res = num2 / num1;
break;
default:
throw new RuntimeException("运算符有误~");
}
stack.push(res + "");
}
}
return Integer.valueOf(stack.pop());
}
}
// 编写一个枚举类定义优先级
enum Operation{
ADD(1),
SUB(1),
MUL(2),
DIV(2);
private final int priority;
Operation(int priority){
this.priority = priority;
}
public static int getPriority(String operation) {
int result = 0;
switch (operation) {
case "+":
result = ADD.priority;
break;
case "-":
result = SUB.priority;
break;
case "*":
result = MUL.priority;
break;
case "/":
result = DIV.priority;
break;
default:
System.out.println("不存在该运算符");
break;
}
return result;
}
}
递归:
# 概念:
以此类推是递归的基本思想。
具体来讲就是把规模大的问题转化为规模小的相似的子问题来解决。在函数实现时,因为解决大问题的方法和解决小问题的方法往往是同一个方法,所以就产生了函数调用它自身的情况。另外这个解决问题的函数必须有明显的结束条件,这样就不会产生无限递归的情况了。
# 递归的两个条件:
1. 可以通过递归调用来缩小问题规模,且新问题与原问题有着相同的形式。(自身调用)
2. 存在一种简单情境,可以使递归在简单情境下退出。(递归出口)
# 递归三要素:
1. 一定有一种可以退出程序的情况;
2. 总是在尝试将一个问题化简到更小的规模
3. 父问题与子问题不能有重叠的部分
# 递归中的“递”就是入栈,递进;“归”就是出栈,回归。
### 递归实际案例:迷宫问题
## 思路分析:
# 使用递归回溯给小球找路
1. map表示地图
2. i,j 表示从地图哪个位置开始出发(1,1)
3. 如果小球能到 map[6][5]位置,则说明通路找到
4. 约定:当map[i][j]=0表示该点没走过,1:墙,2:通路可以走,3:该点已走过,但是走不通.
5. 制定策略(走的顺序): 下-右-上-左,如果该点走不通,再回溯.
## 代码实现:
public class MiGong {
public static void main(String[] args) {
// 创建一个二维数组,模拟迷宫
int[][] map = new int[8][7];
// 1:墙
// 上下置为1
for (int i = 0; i < 7; i++) {
map[0][i] = 1;
map[7][i] = 1;
}
// 左右置为1
for (int i = 0; i < 8; i++) {
map[i][0] = 1;
map[i][6] = 1;
}
// 加挡板
map[3][1] = 1;
map[3][2] = 1;
// map[1][2] = 1;
// map[2][2] = 1;
// 输出地图
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 7; j++) {
System.out.print(map[i][j] + " ");
}
System.out.println();
}
System.out.println();
setWay(map, 1, 1);
// 输出地图
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 7; j++) {
System.out.print(map[i][j] + " ");
}
System.out.println();
}
}
/**
*
* @param map 表示地图
* @param i 从哪个位置开始
* @param j
* @return 如果找到通路,就返回true,否则返回false
*/
public static boolean setWay(int[][] map, int i, int j){
if(map[6][5] == 2){// 通路已经找到
return true;
}else {
if(map[i][j] == 0){// 当前点还未走过
// 按照策略 下-右-上-左 走
map[i][j] = 2;// 假定该点可以走通
if(setWay(map, i+1, j)){// 向下走
return true;
}else if(setWay(map, i, j+1)){// 向右走
return true;
}else if(setWay(map, i-1, j)){// 向上走
return true;
}else if(setWay(map, i, j-1)){// 向左走
return true;
}else {
// 说明该点走不通,是死路
map[i][j] = 3;
return false;
}
}else {// map[i][j] 可能为 1 2 3
return false;
}
}
}
}
#### 八皇后问题(回溯算法)
## 代码实现:
public class Queue8 {
int max = 8;// 定义共有多少个皇后
int[] array = new int[max];// 装载皇后放置的位置结果
static int count = 0;
public static void main(String[] args) {
Queue8 queue8 = new Queue8();
queue8.check(0);
System.out.printf("一共有%d种方法", count);
}
private void check(int n){
if(n == max){
print();
return;
}
// 依次放入皇后,检测是否冲突
for (int i = 0; i < max; i++) {
// 先把当前皇后n,放到该行的第1列
array[n] = i;
// 判断当放置第n个皇后到i列时,是否冲突
if(judge(n)){
check(n+1);
}
// 如果冲突则继续执行array[n] = i
// 即将第n个皇后,放置在本行的后移的一个位置
}
}
/**
* 查看当放置第n个皇后,就去检测该皇后是否是前面已经摆放的冲突
* @param n 表示n个皇后
* @return
*/
private boolean judge(int n){
for (int i = 0; i < n; i++) {
// 说明:
// 1. array[i] == array[n],判断第n个是否和前面的在同一行
// 2. Math.abs(n-i)==Math.abs(array[n]-array[i])),
// 判断第n个是否和第i皇后在同一斜线(斜率相等就成立)
// 3. 没有必要判断是否在同一行,n是递增的
if(array[i] == array[n] || Math.abs(n-i)==Math.abs(array[n]-array[i])){
return false;
}
}
return true;
}
/**
* 打印
*/
private void print(){
for (int i : array) {
System.out.print(i + " ");
}
count++;
System.out.println();
}
}
哈希表:
# 哈希表(散列):
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构.也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度.这个映射函数叫做散列函数,存放记录的数组叫做散列表.
### 代码实现
public class HashtabDemo {
public static void main(String[] args) {
// 创建哈希表
HashTab hashTab = new HashTab(7);
String key = "";
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("请输入对应命令");
System.out.println("add: 添加数据");
System.out.println("list: 展示所有数据");
System.out.println("find: 搜索指定id信息");
System.out.println("exit: 退出程序");
key = scanner.next();
switch (key) {
case "add":
System.out.println("请输入id");
int id = scanner.nextInt();
System.out.println("请输入名字");
String name = scanner.next();
Emp emp = new Emp(id, name);
hashTab.add(emp);
break;
case "list":
hashTab.list();
break;
case "find":
System.out.println("请输入id");
id = scanner.nextInt();
hashTab.findId(id);
break;
case "exit":
scanner.close();
default:
break;
}
}
}
}
class HashTab{
private EmpLinkedList[] empLinkedLists;
private int size;// 表示有多少条链表
// 构造器
public HashTab(int size){
this.size = size;
empLinkedLists = new EmpLinkedList[size];
for (int i = 0; i < size; i++) {
empLinkedLists[i] = new EmpLinkedList();
}
}
// 添加
public void add(Emp emp){
int empListNo = hashFun(emp.id);
empLinkedLists[empListNo].add(emp);
}
// 遍历
public void list(){
for (int i = 0; i < size; i++) {
empLinkedLists[i].list(i);
}
}
// 找到指定的id信息
public void findId(int no){
for (int i = 0; i < size; i++) {
empLinkedLists[i].selectById(no);
}
}
// 编写散列函数,使用 取模 方法
public int hashFun(int id){
return id % size;
}
}
class Emp{
public int id;
public String name;
public Emp next;// next 默认为null
public Emp(int id, String name){
super();
this.id = id;
this.name = name;
}
}
// 创建 EmpLinkedList, 表示链表
class EmpLinkedList{
// 头指针,链表的head,指向第一个Emp
private Emp head;
// 添加
public void add(Emp emp){
if(head == null){
head = emp;
return;
}
Emp curEmp = head;// 辅助指针
boolean flag = false;// 判断id是否存在
while (true) {
if(curEmp.next == null){
break;
}
// 如果id相同, 则不能添加
if(curEmp.next.id == emp.id){
flag = true;
break;
}
// 根据 id 大小排序插入
if(curEmp.next.id > emp.id){
break;
}
curEmp = curEmp.next;
}
// 判断 flag 的值
if(flag){
System.out.printf("准备插入的id %d 已经存在,不能加入了\n", emp.id);
}else {
// 插入到链表中,temp的后面
emp.next = curEmp.next;
curEmp.next = emp;
}
}
// 遍历链表信息
public void list(int no){
if(head == null){
System.out.println("第 " + (no+1) + " 链表为空");
return;
}
System.out.print("第 " + (no+1) + " 链表信息为 ");
Emp curEmp = head;
while (true) {
System.out.printf("=> id = %d, name = %s \t", curEmp.id, curEmp.name);
if(curEmp.next == null){
break;
}
curEmp = curEmp.next;
}
System.out.println();
}
// 查找指定 no 信息
public void selectById(int no){
if(head == null){
System.out.println("第 " + (no+1) + " 链表为空");
return;
}
Emp curEmp = head;
while (true) {
if(curEmp.id == no){
System.out.println("找到该id信息:" + curEmp.id + " => name: " + curEmp.name);
break;
}
if(curEmp.next == null){
System.out.println("没有此" + no + "的信息");
break;
}
curEmp = curEmp.next;
}
}
}
树结构:
# 为什么需要树这种数据结构?
1. 数组存储方式的分析:(ArrayList默认容量10)
优点:通过下标方式访问元素,速度快.对于有序数组,还可以使用二分查找提高检索速度.
缺点:如果要检索具体某个值,或者插入值(按一定顺序)会整体移动,效率较低.
2. 链式存储方式的分析:
优点:在一定程度上对数组存储方式有优化(比如:插入一个数值节点,只需要将插入节点,链接到链表即可,删除效率也很好).
缺点:在进行检索时,效率仍然较低,比如(检索某个值,需要从头节点开始遍历).
3. 树存储方式的分析:
能提高数据存储,读取的效率,比如利用二叉排序树,既可以保证数据的检索速度,同时也可以保证数据的插入没删除,修改的速度.
# ArrayList的底层操作机制源码分析:
1. ArrayList中维护了一个Object类型的数组 elementData
2. 当创建对象时,如果使用的是无参构造器,则初始elementData 容量为0
3. 如果使用的是指定容量 capacity 构造器,则初始elementData 容量为 capacity
4. 当添加元素时:先判断是否需要扩容,如果需要扩容,则调用 grow 方法,否则直接添加元素到合适的位置
5. 如果使用的是无参构造器,如果第一次添加,需要扩容的话,则扩容 elementData 为 10,如果需要再次扩容的话,则扩容 elementData 的1.5倍.
6. 如果使用的是指定容量 capacity 的构造器,如果需要扩容,则直接扩容 elementData 的1.5倍.
二叉树:
# 介绍:
1. 二叉树(binary tree)是指树中节点的度不大于2的有序树,它是一种最简单且最重要的树。二叉树的递归定义为:二叉树是一棵空树,或者是一棵由一个根节点和两棵互不相交的,分别称作根的左子树和右子树组成的非空树;左子树和右子树又同样都是二叉树
2. 每个节点最多只能有两个子节点的一种形式称为二叉树
3. 如果该二叉树的所有叶子节点都在最后一层,并且节点总数=2^n-1,n为层数,则称为 '满二叉树'.
4. 如果该二叉树的所有叶子节点都在最后一层或倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,称为 '完全二叉树'.
# 树的常用术语:
节点、根节点、父节点、子节点、叶子节点(没有子节点的节点)、节点的权(节点值:数据内容)、路径(从根节点找到该接待你的路线)、层、子树、树的高度(最大层数)、森林(多颗子树构成森林).
二叉树的增删改查:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rY0sWEMM-1631513074147)(C:\Users\12271\Desktop\MD格式笔记\图片存放\二叉树中序遍历.png)]
# 二叉树遍历:(上图为中序遍历图)
前序遍历:'先输出父节点',再遍历左子树和右子树
中序遍历:先遍历左子树,'再输出父节点',再遍历右子树
后序遍历:先遍历左子树,再遍历右子树,'最后输出父节点'
小结:看输出父节点的顺序,就确定是前序,中序还是后序.
# 前序遍历:
1. 先输出当前节点(初始的时候是root节点)
2. 如果左子节点不为空,则递归继续前序遍历
3. 如果右子节点不为空,则递归继续前序遍历
# 中序遍历:
1. 如果当前节点左子节点不为空,则递归中序遍历
2. 输出当前节点
3. 如果当前节点右子节点不为空,则递归中序遍历
# 后序遍历:
1. 如果当前节点左子节点不为空,则递归后序遍历
2. 如果当前节点右子节点不为空,则递归后序遍历
3. 输出当前节点
### 代码实现(二叉树的遍历)
public class BinaryTreeDemo {
public static void main(String[] args) {
BinaryTree binaryTree = new BinaryTree();
HeroNode node1 = new HeroNode(1, "迪迦奥特曼");
HeroNode node2 = new HeroNode(2, "泰罗奥特曼");
HeroNode node3 = new HeroNode(3, "戴拿奥特曼");
HeroNode node4 = new HeroNode(4, "雷欧奥特曼");
HeroNode node5 = new HeroNode(5, "艾斯奥特曼");
// 建立二叉树
binaryTree.setRoot(node1);
node1.setLeft(node2);
node1.setRight(node3);
node3.setLeft(node4);
node3.setRight(node5);
System.out.println("前序遍历");
binaryTree.preOrder();
System.out.println("中序遍历");
binaryTree.midOrder();
System.out.println("后序遍历");
binaryTree.postOrder();
}
}
// 定义BinaryTree 二叉树
@Data
class BinaryTree{
private HeroNode root;
// 调用前序遍历
public void preOrder(){
if(this.root != null){
this.root.preOrder();
}else {
System.out.println("二叉树为空,无法遍历");
}
}
// 调用中序遍历
public void midOrder(){
if(this.root != null){
this.root.midOrder();
}else {
System.out.println("二叉树为空,无法遍历");
}
}
// 调用后序遍历
public void postOrder(){
if(this.root != null){
this.root.postOrder();
}else {
System.out.println("二叉树为空,无法遍历");
}
}
}
// 创建 HeroNode 节点
@Data
class HeroNode{
private int no;
private String name;
private HeroNode left;
private HeroNode right;
public HeroNode(int no, String name) {
this.no = no;
this.name = name;
}
@Override
public String toString() {
return "HeroNode{" + "no=" + no + ", name='" + name + '\'' + '}';
}
// 前序遍历
public void preOrder(){
// 输出父节点
System.out.println(this);
// 递归向左子树前序遍历
if(this.left != null){
this.left.preOrder();
}
// 递归向右子树前序遍历
if(this.right != null){
this.right.preOrder();
}
}
// 中序遍历
public void midOrder(){
// 递归向左子树前序遍历
if(this.left != null){
this.left.midOrder();
}
// 输出父节点
System.out.println(this);
// 递归向右子树前序遍历
if(this.right != null){
this.right.midOrder();
}
}
// 后续遍历
public void postOrder(){
// 递归向左子树前序遍历
if(this.left != null){
this.left.postOrder();
}
// 递归向右子树前序遍历
if(this.right != null){
this.right.postOrder();
}
// 输出父节点
System.out.println(this);
}
}
# 利用二叉树的 前序、中序、后序 查找指定节点数据
### 代码实现
public class BinaryTreeDemo {
public static void main(String[] args) {
BinaryTree binaryTree = new BinaryTree();
HeroNode node1 = new HeroNode(1, "迪迦奥特曼");
HeroNode node2 = new HeroNode(2, "泰罗奥特曼");
HeroNode node3 = new HeroNode(3, "戴拿奥特曼");
HeroNode node4 = new HeroNode(4, "雷欧奥特曼");
HeroNode node5 = new HeroNode(5, "艾斯奥特曼");
// 建立二叉树
binaryTree.setRoot(node1);
node1.setLeft(node2);
node1.setRight(node3);
node3.setLeft(node4);
node3.setRight(node5);
System.out.println("前序遍历查找");
System.out.println(binaryTree.preOrderSearch(7));
System.out.println("中序遍历查找");
System.out.println(binaryTree.midOrderSearch(3));
System.out.println("后序遍历查找");
System.out.println(binaryTree.postOrderSearch(3));
}
}
// 定义BinaryTree 二叉树
@Data
class BinaryTree{
private HeroNode root;
// 调用前序遍历查找
public HeroNode preOrderSearch(int no){
if(root != null){
return root.preOrderSearch(no);
}else {
return null;
}
}
// 调用中序遍历查找
public HeroNode midOrderSearch(int no){
if(this.root != null){
return this.root.midOrderSearch(no);
}else {
return null;
}
}
// 调用后序遍历查找
public HeroNode postOrderSearch(int no){
if(this.root != null){
return this.root.postOrderSearch(no);
}else {
return null;
}
}
}
// 创建 HeroNode 节点
@Data
class HeroNode{
private int no;
private String name;
private HeroNode left;
private HeroNode right;
public HeroNode(int no, String name) {
this.no = no;
this.name = name;
}
@Override
public String toString() {
return "HeroNode{" + "no=" + no + ", name='" + name + '\'' + '}';
}
// 前序遍历查找
public HeroNode preOrderSearch(int no){
if(this.no == no){
return this;
}
HeroNode resNode = null;
if(this.left != null){
resNode = this.left.preOrderSearch(no);
}
if(resNode != null){
return resNode;
}
if(this.right != null){
resNode = this.right.preOrderSearch(no);
}
return resNode;
}
// 中序遍历查找
public HeroNode midOrderSearch(int no){
HeroNode resNode = null;
if(this.left != null){
resNode = this.left.midOrderSearch(no);
}
if(resNode != null){
return resNode;
}
if(this.no == no){
return this;
}
if(this.right != null){
resNode = this.right.midOrderSearch(no);
}
return resNode;
}
// 后序遍历查找
public HeroNode postOrderSearch(int no){
HeroNode resNode = null;
if(this.left != null){
resNode = this.left.postOrderSearch(no);
}
if(resNode != null){
return resNode;
}
if(this.right != null){
resNode = this.right.postOrderSearch(no);
}
if(resNode != null){
return resNode;
}
if(this.no == no){
return this;
}
return resNode;
}
}
## 二叉树-删除节点
1. 如果删除的节点是叶子节点,则删除节点
2. 如果删除的节点是非叶子节点,则删除该子树
因为二叉树为单向的,所以当前节点找不到上一节点,所以需要判断当前节点的下一节点是否为需要删除的节点,如果是删除节点则将当前节点的 下一节点 置为null.
# 思路:
1. 如果当前结点的左子结点不为空。并且左子结点是删除结点,就将 this.left = null,并返回
2. 如果当前结点的右子结点不为空。并且右子结点是删除结点,就将 this.right = null,并返回
3. 如果 1、2 没有删除结点,那么就向左子树进行递归删除
4. 如果 3 也没有删除结点,就向右子树进行递归删除
### 代码实现
public class BinaryTreeDemo {
public static void main(String[] args) {
BinaryTree binaryTree = new BinaryTree();
HeroNode node1 = new HeroNode(1, "迪迦奥特曼");
HeroNode node2 = new HeroNode(2, "泰罗奥特曼");
HeroNode node3 = new HeroNode(3, "戴拿奥特曼");
HeroNode node4 = new HeroNode(4, "雷欧奥特曼");
HeroNode node5 = new HeroNode(5, "艾斯奥特曼");
// 建立二叉树
binaryTree.setRoot(node1);
node1.setLeft(node2);
node1.setRight(node3);
node3.setLeft(node4);
node3.setRight(node5);
binaryTree.delNo(4);
binaryTree.preOrder();
}
}
// 定义BinaryTree 二叉树
@Data
class BinaryTree{
private HeroNode root;
// 调用前序遍历
public void preOrder(){
if(this.root != null){
this.root.preOrder();
}else {
System.out.println("二叉树为空,无法遍历");
}
}
// 删除结点
public void delNo(int no){
if(root != null){
if(root.getNo() == no){
root = null;
}else {
root.delNo(no);
}
}else {
System.out.println("空树,不能删除");
}
}
}
// 创建 HeroNode 节点
@Data
class HeroNode{
private int no;
private String name;
private HeroNode left;
private HeroNode right;
public HeroNode(int no, String name) {
this.no = no;
this.name = name;
}
@Override
public String toString() {
return "HeroNode{" + "no=" + no + ", name='" + name + '\'' + '}';
}
// 前序遍历
public void preOrder(){
// 输出父节点
System.out.println(this);
// 递归向左子树前序遍历
if(this.left != null){
this.left.preOrder();
}
// 递归向右子树前序遍历
if(this.right != null){
this.right.preOrder();
}
}
// 删除结点
public void delNo(int no){
if(this.left != null && this.left.no == no){
this.left = null;
return;
}
if(this.right != null && this.right.no == no){
this.right = null;
return;
}
if(this.left != null){
this.left.delNo(no);
}
if(this.right != null){
this.right.delNo(no);
}
}
}
顺序存储二叉树:
# 概念:
从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转换成树,树也可以转换成数组.
# 顺序存储二叉树的特点:
1. 顺序二叉树通常只考虑完全二叉树
2. 第n个元素的左子节点为 2*n+1
3. 第n个元素的右子节点为 2*n+2
4. 第n个元素的父结点为 (n-1)/2
5. n: 表示二叉树中的第几个元素(按0开始编号)
### 代码实现(顺序存储二叉树前序遍历)
public class ArrBinaryTreeDemo {
public static void main(String[] args) {
int []arr = {1, 2, 3, 4, 5, 6, 7};
ArrBinaryTree arrBinaryTree = new ArrBinaryTree(arr);
arrBinaryTree.preOrder();// 1245367
}
}
class ArrBinaryTree{
private int[] arr;
public ArrBinaryTree(int[] arr){
this.arr = arr;
}
public void preOrder(){
this.preOrder(0);
}
// 顺序存储二叉树的前序遍历
public void preOrder(int index){
// 如果数组为空,或者arr.length = 0
if(arr == null || arr.length == 0){
System.out.println("数组为空,不能遍历");
}
// 输出当前元素
System.out.println(arr[index]);
// 向左递归遍历
if((index * 2 + 1) < arr.length){
preOrder(index * 2 + 1);
}
// 向右递归遍历
if((index * 2 + 2) < arr.length){
preOrder(index * 2 + 2);
}
}
}
线索化二叉树:
# 介绍
在二叉树的结点上加上线索的二叉树称为线索二叉树,对二叉树以某种遍历方式(如先序、中序、后序或层次等)进行遍历,使其变为线索二叉树的过程称为对二叉树进行线索化
1. 对于n个结点的二叉树,在二叉链存储结构中有n+1个空链域,利用这些空链域存放在某种遍历次序下该结点的前驱结点和后继结点的指针,这些指针称为线索,加上线索的二叉树称为线索二叉树。
2. 这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种
3. 一个结点的前一个结点,称为前驱结点
4. 一个结点的后一个结点,称为后继结点
注意:线索链表解决了无法直接找到该结点在某种遍历序列中的前驱和后继结点的问题,解决了二叉链表找左、右孩子困难的问题。
# 遍历线索化二叉树
说明:对前面的中序线索化的二叉树进行遍历
分析:因为线索化后,各个结点指向有变化,因此原来的遍历方式不能使用,这时需要使用新的方式遍历线索化二叉树,各个节点可以通过线性方式遍历,因此无需使用递归方式,提高了遍历的效率。遍历的次序应当和中序遍历保持一致.
### 代码实现(中序遍历)
public class ThreadedBinaryTreeDemo {
public static void main(String[] args) {
HeroNode1 root = new HeroNode1(1, "tom");
HeroNode1 node2 = new HeroNode1(3, "jack");
HeroNode1 node3 = new HeroNode1(6, "smith");
HeroNode1 node4 = new HeroNode1(8, "mary");
HeroNode1 node5 = new HeroNode1(10, "king");
HeroNode1 node6 = new HeroNode1(14, "dim");
root.setLeft(node2);
root.setRight(node3);
node2.setLeft(node4);
node2.setRight(node5);
node3.setLeft(node6);
ThreadedBinaryTree threadedBinaryTree = new ThreadedBinaryTree();
threadedBinaryTree.setRoot(root);
threadedBinaryTree.threadedNodes();
// 测试:以10号结点测试
HeroNode1 leftNode = node5.getLeft();
HeroNode1 rightNode = node5.getRight();
System.out.println("10号前驱结点是:" + leftNode);
System.out.println("10号后继结点是:" + rightNode);
threadedBinaryTree.threadedList();// 8 3 10 1 14 6
}
}
class ThreadedBinaryTree{
private HeroNode1 root;
// 保留前面遍历过的节点
private HeroNode1 pre;
public void setRoot(HeroNode1 root){
this.root = root;
}
// 方法重载
public void threadedNodes(){
this.threadedNodes(root);
}
// 编写对二叉树进行中序线索化的方法
public void threadedNodes(HeroNode1 node){
if(node == null){
return;
}
// 线索化左子树
threadedNodes(node.getLeft());
// 处理当前节点的前驱节点
if(node.getLeft() == null){
// 让当前节点的左指针指向前驱节点
node.setLeft(pre);
// 修改当前节点的左指针的类型,指向前驱结点
node.setLeftType(1);
}
// 处理后继结点
if(pre != null && pre.getRight() == null){
// 让前驱结点的右指针指向当前结点
pre.setRight(node);
// 修改前驱结点的右指针类型
pre.setRightType(1);
}
// 将保留结点同步
pre = node;
// 线索化右子树
threadedNodes(node.getRight());
}
// 遍历线索化二叉树的方法
public void threadedList() {
HeroNode1 node = root;
while (node != null) {
while (node.getLeftType() == 0) {
node = node.getLeft();
}
// 打印
System.out.println(node);
// 如果当前结点的右指针指向的是后继结点,就一直输出
while (node.getRightType() == 1) {
node = node.getRight();
System.out.println(node);
}
// 替换这个遍历的结点
node = node.getRight();
}
}
}
@Data
class HeroNode1{
private int no;
private String name;
private HeroNode1 left;
private HeroNode1 right;
// 1. 如果leftType==0 表示指向的是左子树,如果1则指向前驱节点
// 2. 如果rightType==0 表示指向的是右子树,如果1则指向后继节点
private int leftType;
private int rightType;
public HeroNode1(int no, String name) {
this.no = no;
this.name = name;
}
@Override
public String toString() {
return "HeroNode1{" +
"no=" + no +
", name='" + name + '\'' +
'}';
}
}
树结构的实际应用:
堆排序:
# 堆排序的基本介绍:
1. 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏、最好、平均复杂度均为O(nlogn),它也是不稳定排序.
2. 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆,注意:没有要求结点的左孩子的值和右孩子的值的大小关系.
3. 每个结点的值都小于或等于左右孩子结点的值,称为小顶堆.
4. 大顶堆特点:arr[i] >= arr[2*i+1] && arr[i] >= arr[2*i+2],// i对应第几个结点,i从0开始编号.
5. 小顶堆特点:arr[i] <= arr[2*i+1] && arr[i] <= arr[2*i+2],// 一般升序采用大顶堆,降序采用小顶堆
# 堆排序基本思想:
1. 将待排序序列构造成一个大顶堆.
2. 此时,整个序列的最大值就是堆顶的根节点.
3. 将其与末尾元素进行交换,此时末尾就为最大值.
4. 然后将剩余n-1个元素重新构成一个堆,这样会得到n个元素的次小值,如此反复执行,便能得到一个有序序列了.
### 代码实现:
public class HeapSort {
public static void main(String[] args) {
int arr[] = {4, 6, 8, 5, 9, -1, -2, -5, 7};
heapSort(arr);
}
public static void heapSort(int arr[]){
System.out.println("堆排序");
// 分步完成
// adjustHeap(arr, 1, arr.length);
// System.out.println(Arrays.toString(arr));
// adjustHeap(arr, 0, arr.length);
// System.out.println(Arrays.toString(arr));
// 将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆
for (int i = arr.length/2-1; i >= 0; i--) {
adjustHeap(arr, i, arr.length);
}
int temp = 0;
// 将堆顶元素与末尾元素交换,将最大元素‘沉’到数组末端
// 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与
// 当前末尾元素,反复执行调整+交换步骤,直到整个序列有序
for (int j = arr.length-1; j > 0; j--) {
// 交换
temp = arr[j];
arr[j] = arr[0];
arr[0] = temp;
adjustHeap(arr, 0, j);
}
System.out.println(Arrays.toString(arr));
}
/**
* 功能:完成将以 i 对应的非叶子结点的树调整成大顶堆
* 举例:int arr[] = {4, 6, 8, 5, 9};=> i=1 => {4, 9, 8, 5, 6}
* i=0 => {9, 6, 8, 5, 4}
* @param arr 待调整数组
* @param i 非叶子结点在数组中索引
* @param length 表示对多少个元素进行调整
*/
public static void adjustHeap(int arr[], int i, int length){
int temp = arr[i];
// 开始调整
// k*2+1,k 是 i结点的左子节点
for (int k = i*2+1; k < length; k = k*2+1) {
if(k+1 < length && arr[k] < arr[k+1]){
k++;// 指向右子节点
}
if(arr[k] > temp){// 子结点大于父结点
arr[i] = arr[k];// 值交换
i = k;// i 指向 k,继续循环比较
}else {
break;
}
}
// 当 for 循环结束,将 i 为父节点的树的最大值,放在顶部
arr[i] = temp;
}
}
赫夫曼树:
=>
# 基本介绍:
1. 给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(HuffmanTree)(霍夫曼树).
2. 赫夫曼树是带权路径长度最短的树:权值较大的结点离根越近的二叉树才是最优二叉树.
# 相关概念:
1. 路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1。
2. 结点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积。
3. 树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL。
4. WPL最小的就是赫夫曼树.
### 代码实现:(创建赫夫曼树)
public class HuffmanTree {
public static void main(String[] args) {
int []arr = {8,11,23,29,14,7,3,5};
Node node = huffman(arr);
node.frontShow();
}
private static Node huffman(int []arr){
// 创建一个集合存储二叉树
List<Node> nodes = new ArrayList<>();
for (int value : arr) {
nodes.add(new Node(value));
}
while (nodes.size() > 1) {
// 对集合进行排序
Collections.sort(nodes);
// 取出最小的结点
Node left = nodes.get(nodes.size()-1);
// 取出次小的结点
Node right = nodes.get(nodes.size()-2);
// 新建一个根结点
Node parent = new Node(left.value + right.value);
parent.left = left;
parent.right = right;
// 删除两个结点
nodes.remove(left);
nodes.remove(right);
// 将根节点添加到集合中
nodes.add(parent);
}
return nodes.get(0);
}
}
class Node implements Comparable<Node>{
int value;
Node left;
Node right;
public Node(int value){
this.value = value;
}
@Override
public int compareTo(Node o) {//
return -(this.value - o.value);
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
// 前序遍历
public void frontShow(){
System.out.println(value);
if(left != null){
left.frontShow();
}
if(right != null){
right.frontShow();
}
}
}
赫夫曼编码:
# 基本介绍:
1. 赫夫曼编码(哈夫曼编码)(Huffman Coding),又称霍夫曼编码,是一种编码方式,属于一种程序算法.
2. 赫夫曼编码是赫夫曼树在电讯通信中的经典的应用之一.
3. 赫夫曼编码广泛用于数据文件压缩,其压缩率通常在20%~90%之间.
4. 赫夫曼码是可变长编码(VLC)的一种,Huffman于1952年提出一种编码方法,称之为最佳编码.
### 通信领域中信息的处理方式:
# 1. 定长编码:
字符对应Ascii码,然后转为对应的二进制,按照二进制来传递信息.
# 2. 变长编码:
按照各个字符出现的次数进行编码,原则是出现次数越多的,则编码越小,比如空格出现9次,编码为0,其它依次类推.
# 3. 赫夫曼编码:(属于变长编码(VLC)的一种)
# 赫夫曼编码原理剖析:
注意,这个赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是wpl是一样的,都是最小的。所以最后编码也会一致.生成的赫夫曼编码长度一致
# 前缀编码:
字符的编码都不能是其它字符编码的前缀,符合此要求的编码叫做前缀编码,即不能匹配到重复的编码
### 赫夫曼编码压缩文件注意事项:
1. 如果文件本身就是经过压缩处理的,那么使用赫夫曼编码再压缩,效率不会有明显变化
2. 赫夫曼编码是按字节来处理的,因此可以处理所有的文件(二进制文件、文本文件)
3. 如果一个文件中的内容,重复数据不多,压缩效果也不会很明显
### 最佳实践-数据压缩与解压(创建赫夫曼树)
### 生成赫夫曼编码和赫夫曼编码后的数据
public class HuffmanCode {
public static void main(String[] args) {
// String content = "i like like like java do you like a java";
// byte[] contentBytes = content.getBytes();
// System.out.println(contentBytes.length);
//
// List<HuffCode> huffCodes = getHuffCode(contentBytes);
// System.out.println(huffCodes);
//
// System.out.println("赫夫曼树");
// HuffCode huffCode = createHuffmanTree(huffCodes);
// System.out.println(huffCode);
// System.out.println("前序遍历");
// preOrder(huffCode);
//
// // 测试是否生成了对应的赫夫曼编码
// getCodes(huffCode, "", stringBuilder);
// System.out.println("生成的赫夫曼编码表 : " + huffmanCodes);
//
// getCodes(huffCode);
// System.out.println("重载方法:" + getCodes(huffCode));
//
// byte[] huffmanCodeBytes = zip(contentBytes, getCodes(huffCode));
// System.out.println("huffmanCodeBytes :" + Arrays.toString(huffmanCodeBytes));
// byte[] huffmanCodeBytes2 = zip(content);
// System.out.println("封装之后的方法调用:" + Arrays.toString(huffmanCodeBytes2));
//
// byte[] decode = decode(huffmanCodes, huffmanCodeBytes2);
// System.out.println(Arrays.toString(decode));
// System.out.println(new String(decode));
// 测试压缩文件
// String src = "C:\\Users\\12271\\Desktop\\斗LD陆【完】.txt";
// String dst = "C:\\Users\\12271\\Desktop\\斗LD陆【完】.zip";
// zipFile(src, dst);
// System.out.println("压缩成功!");
// 测试解压文件
String zipFile = "C:\\Users\\12271\\Desktop\\斗LD陆【完】.zip";
String dstFile = "C:\\Users\\12271\\Desktop\\aa\\a.txt";
unZipFile(zipFile, dstFile);
System.out.println("解压成功!");
}
/**
* 压缩文件
* @param srcFile 传入的需压缩文件的全路径
* @param dstFile 压缩后文件存放的目标目录
*/
private static void zipFile(String srcFile, String dstFile){
// 创建输出流
OutputStream os = null;
ObjectOutputStream oos = null;
// 创建文件输入流
FileInputStream is = null;
try{
// 创建文件的输入流
is = new FileInputStream(srcFile);
// 创建一个和源文件大小一样的byte[]
byte[] b = new byte[is.available()];
// 读取文件
is.read(b);
// 直接对源文件压缩
byte[] huffmanBytes = huffmanZip(b);
// 创建文件的输出流,存放压缩文件
os = new FileOutputStream(dstFile);
// 创建一个和文件输出流关联的ObjectOutputStream
oos = new ObjectOutputStream(os);
// 把 赫夫曼编码后对应的字节数组写入压缩文件
oos.writeObject(huffmanBytes);
// 把 赫夫曼编码表写入
oos.writeObject(huffmanCodes);
}catch (Exception e){
System.out.println(e.getMessage());
}finally {
try {
// 关闭流
is.close();
oos.close();
os.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
/**
* 完成对文件的解压
* @param zipFile 需要解压的文件
* @param dstFile 解压的路径
*/
private static void unZipFile(String zipFile, String dstFile){
// 定义文件输入流
InputStream is = null;
// 定义一个对象输入流
ObjectInputStream ois = null;
// 定义文件的输出流
OutputStream os = null;
try{
// 创建文件输入流
is = new FileInputStream(zipFile);
// 创建一个和 is 关联的对象输入流
ois = new ObjectInputStream(is);
// 读取byte数组 huffmanBytes
byte[] huffmanBytes = (byte[])ois.readObject();
// 读取赫夫曼编码表
Map<Byte, String> huffmanCodes = (Map<Byte, String>)ois.readObject();
// 解码
byte[] bytes = decode(huffmanCodes, huffmanBytes);
// 将 bytes数组写入到目标文件
os = new FileOutputStream(dstFile);
// 写数据到 dstFile 文件
os.write(bytes);
}catch (Exception e){
System.out.println(e.getMessage());
}finally {
try{
os.close();
ois.close();
is.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
/**
* 解码
* @param huffmanCodes 赫夫曼编码表 map
* @param huffmanBytes 赫夫曼编码得到的字节数组
* @return 原来的字符串对应的数组
*/
private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes){
// 1. 先得到 huffmanBytes 对应的二进制的字符串,形式 1010100010111...
StringBuilder stringBuilder = new StringBuilder();
// 将 byte 数组转成二进制的字符串
for (int i = 0; i < huffmanBytes.length; i++) {
byte b = huffmanBytes[i];
// 判断是不是最后一个字节
boolean flag = (i == huffmanBytes.length - 1);
stringBuilder.append(byteToBitString(!flag, b));
}
System.out.println("打印: " + stringBuilder);
// 把字符串按照指定的赫夫曼编码进行解码
// 把赫夫曼编码表进行调换,因为反向查询 a -> 100 100 -> a
Map<String, Byte> map = new HashMap<>();
for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
map.put(entry.getValue(), entry.getKey());
}
// 创建集合,存放byte
List<Byte> list = new ArrayList<>();
// i 可以理解成就是索引,扫描 stringBuilder
for (int i = 0; i < stringBuilder.length();) {
int count = 1;// 计数器
boolean flag = true;
Byte b = null;
while (flag) {
// i 不动,让 count 动,指定匹配到一个字符
String key = stringBuilder.substring(i, i+count);
b = map.get(key);
if(b == null){
count++;
}else {
flag = false;
}
}
list.add(b);
i += count;
}
// 当for循环结束后,list中就存放了所有的字符
// 把list中的数据放入到byte[] 并返回
byte[] b = new byte[list.size()];
for (int i = 0; i < b.length; i++) {
b[i] = list.get(i);
}
return b;
}
/**
* 将一个 byte 转成一个二进制的字符串
* @param b
* @param flag 标志是否需要补高位,true:需要,false:不需要
* @return
*/
private static String byteToBitString(boolean flag, byte b){
// 使用变量保存b
int temp = b;// 将 b 转成 int
// 如果不是最后一个字节,需要补高位
if(flag){
temp |= 256;// 按位或 1 0000 0000 | 0000 0001 => 1 0000 0001
}
// 返回的是temp对应的二进制的补码
String str = Integer.toBinaryString(temp);
if(flag){
return str.substring(str.length() - 8);
}else {
if(data != null && data.length > 0){
for (byte datum : data) {
str = "0" + str;
}
return str;
}else {
return str;
}
}
}
// 方法整合
private static byte[] huffmanZip(byte[] contentBytes){
// 2. 将bytes统计为一个List
List<HuffCode> huffCodes = getHuffCode(contentBytes);
// 3. 将lists合成一个赫夫曼树
HuffCode huffCode = createHuffmanTree(huffCodes);
// 4. 将赫夫曼树通过规则变成编码
Map<Byte, String> huffmanCodes = getCodes(huffCode);
// 5. 将对应的字符对应规则转化后,再以八位一个进行二进制转换
return zip(contentBytes, huffmanCodes);
}
// 方法整合
private static byte[] zip(String content){
// 1. 将字符串转为bytes
byte[] contentBytes = content.getBytes();
// 2. 将bytes统计为一个List
List<HuffCode> huffCodes = getHuffCode(contentBytes);
// 3. 将lists合成一个赫夫曼树
HuffCode huffCode = createHuffmanTree(huffCodes);
// 4. 将赫夫曼树通过规则变成编码
Map<Byte, String> huffmanCodes = getCodes(huffCode);
// 5. 将对应的字符对应规则转化后,再以八位一个进行二进制转换
return zip(contentBytes, huffmanCodes);
}
/**
* 将字符串对应的byte[]数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码,压缩后的byte[]
* @param bytes 原始的字符串对应的 byte[]
* @param huffmanCodes 生成的赫夫曼比编码 map
* @return 赫夫曼编码处理后的 byte[]
* 举例: String content = "answer"; => byte[] bytes = content.getBytes(); => List<HuffCode> huffCodes = getHuffCode(bytes);
* => HuffCode huffCode = createHuffmanTree(huffCodes); => getCodes(huffCode);
*/
private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes){
// 1. 利用 huffmanCodes 将bytes转成 赫夫曼编码对应的字符串
StringBuilder stringBuilder = new StringBuilder();
// 遍历bytes数组
for (byte b : bytes) {
stringBuilder.append(huffmanCodes.get(b));
}
System.out.println("stringBuilder :" + stringBuilder.toString());
// 统计返回 byte[] huffmanCodes 长度
// int len = (stringBuilder.length() + 7) / 8
int len;
if(stringBuilder.length() % 8 == 0){
len = stringBuilder.length() / 8;
}else{
len = (stringBuilder.length() + 7) / 8 ;
}
// 创建存储压缩后的 byte 数组
byte[] huffmanCodeBytes = new byte[len];
// 记录是第几个byte
int index = 0;
for (int i = 0; i < stringBuilder.length(); i += 8) {
String strByte;
if(i+8 > stringBuilder.length()){// 不够8位
strByte = stringBuilder.substring(i);
int count = 0;
for (int i1 = 0; i1 < strByte.getBytes().length; i1++) {
if(strByte.getBytes()[i1] == 48){
count++;
}else {
break;
}
}
data = new byte[count];
}else {
strByte = stringBuilder.substring(i, i+8);
}
// 将strByte转成一个byte,放入到 huffmanCodeBytes
huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte, 2);
index++;
}
return huffmanCodeBytes;
}
/**
*
* @param bytes 接收字节数组
* @return
*/
private static List<HuffCode> getHuffCode(byte[] bytes){
// 1. 创建一个 ArrayList
List<HuffCode> nodes = new ArrayList();
// 遍历 bytes,统计每一个byte出现的次数 -> map[key, value]
Map<Byte, Integer> counts = new HashMap<>();
for (byte b : bytes) {
Integer count = counts.get(b);
if(count == null){
counts.put(b, 1);
}else {
counts.put(b, ++count);
}
}
// 把每一个键值对转成一个 HuffCode 对象,并加入到集合中
// 遍历map
for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
nodes.add(new HuffCode(entry.getKey(), entry.getValue()));
}
return nodes;
}
// 通过 List 创建对应的 赫夫曼树
private static HuffCode createHuffmanTree(List<HuffCode> nodes){
while(nodes.size() > 1){
// 排序,从大到小
Collections.sort(nodes);
HuffCode left = nodes.get(nodes.size() - 1);
HuffCode right = nodes.get(nodes.size() - 2);
HuffCode parent = new HuffCode(null, left.weight + right.weight);
parent.left = left;
parent.right = right;
nodes.remove(left);
nodes.remove(right);
nodes.add(parent);
}
return nodes.get(0);
}
// 生成赫夫曼树对应的赫夫曼编码
// 思路: 1. 将赫夫曼编码表存放在Map<Byte, String>形式
// 32 -> 01, 97 -> 100, 100 -> 11000 等等形式
static Map<Byte, String> huffmanCodes = new HashMap<>();
// 2. 在生成赫夫曼编码表时,需要去拼接路径,定义一个StringBuilder,存储某个叶子结点的路径
static StringBuilder stringBuilder = new StringBuilder();
// 记录最后八位(可能不足八位) 二进制转十进制 二进制前边的 0, 00110 => 记录 00
static byte[] data;
/**
* 功能:将传入的node结点的所有叶子结点的赫夫曼编码得到,并放入到huffCodes集合
* @param node 传入结点
* @param code 路径:左子节点是0,右子节点是1
* @param stringBuilder 用于拼接路径
*/
private static void getCodes(HuffCode node, String code, StringBuilder stringBuilder){
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
// 将 code 放入到 stringBuilder2
stringBuilder2.append(code);
if(node != null){// 如果是 node==null 不处理
// 判断当前 node 是叶子节点还是非叶子结点
if(node.data == null){// 非叶子结点
// 递归处理,向左递归
getCodes(node.left, "0", stringBuilder2);
// 向右递归
getCodes(node.right, "1", stringBuilder2);
}else {// 说明是一个叶子结点
// 就表示找到某个叶子结点的最后
huffmanCodes.put(node.data, stringBuilder2.toString());
}
}
}
/**
* 重载方法,调用方便
* @param node
* @return
*/
private static Map<Byte, String> getCodes(HuffCode node){
if(node == null){
return null;
}
// 处理左子树
getCodes(node.left, "0", stringBuilder);
// 处理右子树
getCodes(node.right, "1", stringBuilder);
return huffmanCodes;
}
// 前序遍历的方法
private static void preOrder(HuffCode root){
if(root != null){
root.preOrder();
}else {
System.out.println("赫夫曼树为空");
}
}
}
class HuffCode implements Comparable<HuffCode>{
Byte data;// 存放数据(字符)本身,比如 'a' => 97,' ' => 32
int weight;// 权值,表示字符出现的次数
HuffCode left;
HuffCode right;
public HuffCode(Byte data, int weight) {
this.data = data;
this.weight = weight;
}
@Override
public int compareTo(HuffCode o) {
// 从大到小排序
return -(this.weight - o.weight);
}
@Override
public String toString() {
return "HuffCode{" +
"data=" + data +
", weight=" + weight +
'}';
}
// 前序遍历
public void preOrder(){
System.out.println(this);
if(this.left != null){
this.left.preOrder();
}
if(this.right != null){
this.right.preOrder();
}
}
}
二叉排序树:
# 介绍:(BST树)
二叉排序树(Binary Sort Tree),又称二叉查找树(Binary Search Tree),亦称二叉搜索树。是数据结构中的一类。在一般情况下,查询效率比链表结构要高。
对于二叉排序树的任何一个非叶子结点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。特别说明:如果有相同的值,可以将该节点放在左子节点或右子节点。
## 增查 方案分析:
# 使用数组:
1. 数组未排序,优点:直接在数组尾添加,速度快。缺点:查找速度慢
2. 数组排序:优点:可以使用二分查找,查找速度快。缺点:为了保证数组有序,在添加新数据时,找到插入位置后,后面的数据需整体移动,速度慢。
# 使用链式存储-链表:
不管链表是否有序,查找速度都慢,添加数据速度比数组快,不需要数据整体移动。
# 使用二叉排序树:
### 二叉排序树创建/遍历/删除节点
## 二叉树的删除分三种情况:
1. 删除叶子结点
2. 删除只有一颗子树的结点
3. 删除有两颗子树的结点
### 总体代码实现:
public class BinarySortTreeDemo {
public static void main(String[] args) {
int []arr = {7, 3, 10, 12, 5, 1, 9, 2};
BinarySortTree binarySortTree = new BinarySortTree();
// 循环的添加结点到二叉排序树
for (int i = 0; i < arr.length; i++) {
binarySortTree.add(new SortNode(arr[i]));
}
// 中序遍历二叉排序树
System.out.println("中序遍历二叉排序树~");
binarySortTree.infixOrder();
// System.out.println("当删除的结点是叶子结点,删除结点后");
// binarySortTree.delNode(2);
// binarySortTree.delNode(1);
// binarySortTree.infixOrder();
// System.out.println("当删除的结点是存在一个子树,删除结点后");
// binarySortTree.delNode(1);
// binarySortTree.infixOrder();
// System.out.println("当删除的结点是存在两个子树,删除结点后");
// binarySortTree.delNode(7);
// binarySortTree.infixOrder();
}
}
// 创建二叉排序树
class BinarySortTree{
private SortNode root;
// 添加结点的方法
public void add(SortNode node){
if(root == null){
root = node;
}else {
root.add(node);
}
}
// 中序遍历
public void infixOrder(){
if(root != null){
root.infixOrder();
}else {
System.out.println("二叉排序树为空,不能遍历");
}
}
// 查找要删除的结点
public SortNode search(int value){
if(root == null){
return null;
}else {
return root.search(value);
}
}
// 查找父结点
public SortNode searchParent(int value){
if(root == null){
return null;
}else {
return root.searchParent(value);
}
}
/**
* 1. 返回的以node为根结点的二叉排序树左子树的最大结点的值
* 2. 删除node为根节点的二叉树排序树左子树的最大结点
* @param node 传入的结点(当作二叉排序树的根结点)
* @return 返回 左子树最大结点
*/
public int delLeftTreeMax(SortNode node){
SortNode target = node;
// 循环的查找右子节点,找到最大结点
while(target.right != null){
target = target.right;
}
// 这时 target 就指向了最大结点
// 删除最大结点
delNode(target.value);
return target.value;
}
// 删除结点
public void delNode(int value){
if(root == null){
return;
}else {
// 1. 需要先去找到要删除的结点 targetNode
SortNode targetNode = search(value);
// 如果没有找到要删除的结点
if(targetNode == null){
return;
}
// 如果发现当前这颗二叉排序树只有一个结点
if(root.left == null && root.right == null){
root = null;
return;
}
// 去找到 targetNode 的父结点
SortNode parent = searchParent(value);
// 如果要删除的结点是叶子结点
if(targetNode.left == null && targetNode.right == null){
// 判断 targetNode 是父结点的左子结点还是右子结点
if(parent.left != null && parent.left.value == value){// 是左子结点
parent.left = null;
}else if(parent.right != null && parent.right.value == value){// 是右子结点
parent.right = null;
}
}else if(targetNode.left != null && targetNode.right != null){// 删除有两颗子树的结点
int maxVal = delLeftTreeMax(targetNode.left);
targetNode.value = maxVal;
}else {// 删除只有一颗子树的结点
// 如果要删除的结点有左子结点
if(targetNode.left != null){
if(parent != null){// 判断父结点是否为空
if(parent.left.value == value){// 如果 targetNode 是 parent 的左子结点
parent.left = targetNode.left;
}else {// targetNode 是 parent 的右子结点
parent.right = targetNode.left;
}
}else {
root = targetNode.left;
}
}else {// 如果要删除的结点有右子结点
if(parent != null) {// 判断父结点是否为空
// 如果 targetNode 是 parent 的左子结点
if (parent.left.value == value) {
parent.left = targetNode.right;
} else {// 如果 targetNode 是 parent 的右子结点
parent.right = targetNode.right;
}
}else {
root = targetNode.right;
}
}
}
}
}
}
// 创建 Node 结点
class SortNode{
int value;
SortNode left;
SortNode right;
public SortNode(int value){
this.value = value;
}
/**
* 查找要删除的结点
* @param value 希望删除的结点的值
* @return 如果找到返回该结点,否则返回null
*/
public SortNode search(int value){
if(value == this.value){// 找到就是该结点
return this;
}else if(value < this.value){// 向左子树递归查找
// 如果左子结点为空
if(this.left == null){
return null;
}
return this.left.search(value);
}else {// 向右子树递归查找
if(this.right == null){
return null;
}
return this.right.search(value);
}
}
/**
* 查找要删除结点的父节点
* @param value 要找到的结点的值
* @return 要删除的结点的父结点,没有就返回null
*/
public SortNode searchParent(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.searchParent(value);
}else if(value >= this.value && this.right != null){
// 向右子树递归查找
return this.right.searchParent(value);
}else {
// 没有找到父结点
return null;
}
}
}
@Override
public String toString() {
return "SortNode{" +
"value=" + value +
'}';
}
// 添加节点的方法
// 递归的形式添加结点,注意需要满足二叉排序树的要求
public void add(SortNode node){
if(node == null){
return;
}
// 判断传入的结点的值和房前子树的根节点的值关系
if(node.value < this.value){
// 如果当前结点左子结点未null
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);
}
}
}
// 中序遍历
public void infixOrder(){
if(this.left != null){
this.left.infixOrder();
}
System.out.println(this);
if(this.right != null){
this.right.infixOrder();
}
}
}
平衡二叉树:
# 介绍:(AVL树)
1. 平衡二叉树也叫平衡二叉搜索树(Self-balancing Binary Search Tree) 又被称为AVL树,可以保证查询效率较高.
2. 具有以下特点:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。平衡二叉树的常用实现方法是 红黑树、AVL、替罪羊树、Treap、伸展树等。
3. ‘平衡二叉搜索树’是在‘二叉查找树’基础实现的。
# 二叉树存在的问题分析:
1. 左子树全部为空,从形式上看,更像一个单链表
2. 插入速度没有影响
3. 查询速度明显降低(因为需要依次比较),不能发挥BST的优势,因为每次还需要比较左子树,其查询速度比单链表还慢
4. 解决方案-平衡二叉树(AVL)
# AVL树高度求解:
1. 通过递归地调用height(T.left)求出左子树的高度
2. 通过递归的调用height(T.right)求出右子树的高度
3. 利用如下公式求出二叉树的高度:
height(T) = max(height(T.left),height(T.right)) + 1
4. 返回height(T)
# 左旋转:
1. 创建一个新的结点 newNode,值等于当前根节点的值
2. 把新节点的左子树设置为当前节点的左子树
3. 把新节点的右子树设置为当前节点的右子树的左子树
4. 把当前节点的值换为右子节点的值
5. 把当前节点的右子树设置为右子树的右子树
6. 把当前节点的左子树设置为新节点
# 右旋转:
1. 创建一个新的结点 newNode,值等于当前根节点的值
2. 把新节点的右子树设置为当前节点的右子树
3. 把新节点的左子树设置为当前节点的左子树的右子树
4. 把当前节点的值换为左子节点的值
5. 把当前节点的左子树设置为左子树的左子树
6. 把当前节点的右子树设置为新节点
# 双旋转:
问题分析:
1. 当符合右旋转的条件时
2. 如果它的左子树的右子树高度大于它的左子树的高度
3. 先对当前这个节点的左子结点进行左旋转
4. 再对当前节点进行右旋转的操作即可.
多路查找树:
# 多路查找树(B树):
1. 在二叉树中,每个节点有数据项,最多有两个子结点。如果允许每个节点可以有更多的数据项和更多的子结点,就是多叉树
2. 2-3树,2-3-4树就是多叉树,多叉树通过重新组织节点,减少树的高度,能对二叉树进行优化
# B树的基本介绍:
通过重新组织节点,降低树的高度,并且减少i/o读写次数来提升效率
1. 文件系统及数据库系统的设计者利用了磁盘预读原理,将一个节点的大小设为等于一个页(页的大小通常为4K),这样每个节点只需要依次I/O就可以完全载入
2. 将树的度M设置为1024,在600亿个元素中最多只需要4次I/O操作就可以读取到想要的元素,B树(B+)广泛应用于文件存储系统以及数据库系统中
# 二叉树的问题分析:
1. 二叉树需要加载到内存中,如果二叉树的节点很多(比如1亿),即存在如下问题:
2. 问题1:在构建二叉树时,需要多次进行io操作(海量数据存在数据库或文件中),节点海量,构建二叉树时,速度有影响
3. 问题2:节点海量,也会造成二叉树的高度很大,会降低操作速度.
2-3树:
# 基本介绍:
2-3树是最简单的B树结构,具有如下特点:
1. 2-3树的所有叶子节点都在同一层(只要是B树都满足这个条件)
2. 有两个子结点的节点叫二节点,二节点要么没有子节点,要么有两个子结点
3. 有三个子结点的叫三节点,三节点要么没有子结点,要么有三个子结点
4. 2-3树是由二节点和三节点构成的树
B树、B+树和B*树:
# B树的介绍:
B-tree树即B树,B即Balanced.
# B树的说明:
1. B树的阶:节点的最多子结点个数。如果2-3树的阶是3,2-3-4树的阶是4
2. B-树的搜索,从根节点开始,对节点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子节点;重复,直到所对应的儿子指针为空,或已经是叶子节点
3. 关键字集合分布在整颗树中,即叶子节点和非叶子节点都存放数据
4. 搜索有可能在非叶子节点结束
5. 其搜索性能等价于在关键字全集内做一次二分查找
# B+树的介绍:
B+树是B树的变体,也是一种多路搜索树。
# B+树的说明:
1. B+树的搜索与B树的基本相同,区别是B+树只有达到叶子节点才命中(B树可以在非叶子节点命中),其性能也等价于在关键字全集做一次二分查找
2. 所有关键字都出现在叶子节点的链表中(即数据只能在叶子节点【也叫稠密索引】),且链表中的关键字(数据)恰好是有序的
3. 不可能在非叶子节点命中
4. 非叶子节点相当于是叶子节点的索引(稀疏索引),叶子节点相当于是存储(关键字)数据的数据层
5. 更适合文件索引系统
6. B树和B+树各有自己的应用场景,不能说B+树完全比B树好,反之亦然
# B*树的介绍:
B*树是B+树的变体,在B+树的非根和非叶子节点再增加指向兄弟的指针
# B*树的说明:
1. B*树定义了非叶子节点关键字个数至少为(2/3)*M,即块的最低使用率为2/3,而B+树的块的最低使用率为B+树的1/2
2. 从上述一个特点看出,B*树分配新节点的概率比B+树要低,空间使用率更高
图基本介绍:
# 基本介绍:
图是一种数据结构,其中节点可以具有零个或多个相邻元素。两个节点之间的连接称为边。结点也可以称为顶点。
# 图的常用概念:
1.
算法:
排序算法:
# 介绍:
(Sort Algorithm), 排序是将一组数据,依指定的顺序进行排列的过程.
# 排序的分类:
1. 内部排序:指将需要处理的所有数据都加载到内部存储器中进行排序.
2. 外部排序:数据量过大,无法全部加载到内存中,需要借助外部存储进行排序
# 常见的内部排序算法分类:
1. 插入排序:直接插入排序、希尔排序
2. 选择排序:简单选择排序、堆排序
3. 交换排序:冒泡排序、快速排序
4. 归并排序
5. 基数排序
时间复杂度:
# 算法的时间复杂度
# 时间频度:
一个算法中的语句执行次数称为语句频度或时间频度.记为T(n)
特点:忽略常数项、忽略低次项、忽略系数
# 时间复杂度:
算法中的基本操作语句的重复执行次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则成f(n)是T(n)的同数量级函数,记作T(n)=0(f(n))为算法的渐进时间复杂度,简称时间复杂度.
# 常见的时间复杂度:
1. 常数阶O(1)
无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就是O(1).
2. 对数阶O(logN)
在while里面,每次将i*2,乘完之后,i距离n就越近.假设循环x次之后,i>n,那么循环退出,也就是说2的x次方等于n,那么 x=(log2)^n,时间复杂度为O((log2)^n).
3. 线性阶O(n)
for循环里面的代码执行n遍,所以它消耗的时间随着n的变化而变化,因此时间复杂度为O(n).
4. 线性对数阶O(nlogN)
将时间复杂度O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n*O(logN),也就是O(nlogN)
5. 平方阶O(n^2)
把O(n)的代码再嵌套循环一遍,时间复杂度就是O(n^2).
6. 立方阶O(n^3)
7. k次方阶O(n^k)
8. 指数阶O(2^n)
常见的算法时间复杂度由小到大依次为:O(1)<O(log2n)<O(n)<O(nlog2n)<O(n^2)<O(n^3)<O(n^k)<O(2^n),随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低.
## 平均时间复杂度和最坏时间复杂度
平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下,该算法的运行时间.
最坏情况下的时间复杂度称最坏时间复杂度,一般讨论的时间复杂度均是最坏情况下的时间复杂度.这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的界限,这就保证了算法的运行时间不会比最坏情况更长.
平均时间复杂度和最坏时间复杂度是否一致,和算法有关.
## 空间复杂度:
对一个算法在运行过程中临时占用存储空间大小的量度.
做算法分析时,主要在重时间复杂度,为了用户体验,使用空间换时间.(例如:归并排序、快速排序)
冒泡排序:
# 介绍:
时间复杂度 O(n^2)
通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻的元素,若发现逆序则交换,使值较大的元素逐渐从前移向后部
## 代码实现:
public static void main(String[] args) {
//# 冒泡排序:时间复杂度 O(n^2)
int arr[] = {6, 1, -5, 19, 2};
int temp = 0;// 临时变量
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) {
if(arr[j] > arr[j+1]){
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
System.out.println("第" + (i+1) + "次排序后的数组");
System.out.println(Arrays.toString(arr));
}
}
## 优化冒泡排序:
如果中间没有发生交换,那么排序结束(增加flag判断)
public class BubbleSort {
public static void main(String[] args) {
// 冒泡排序:时间复杂度 O(n^2)
int arr[] = {19, 1, -5, 6, 2};
boolean flag = true;// 标志位
int temp = 0;// 临时变量
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) {
if(arr[j] > arr[j+1]){
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
flag = false;
}
}
System.out.println("第" + (i+1) + "次排序后的数组");
System.out.println(Arrays.toString(arr));
if(flag){// 没有发生过交换
break;
}else {
flag = true;// 重置标志位
}
}
}
}
选择排序:
# 介绍:
选择式排序属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,再依次规定交换位置后达到排序的目的.时间复杂度O(n^2)
## 代码实现:
public class SelectSort {
public static void main(String[] args) {
int[] arr = {119, 1, 34, 101};
for (int i = 0; i < arr.length; i++) {
int minIndex = i;
int min = arr[i];
for (int j = i + 1; j < arr.length; j++) {
if(min > arr[j]){// 说明假定的最小值,不是最小
min = arr[j];// 重置 min
minIndex = j;// 重置 minIndex
}
}
// 将最小值放在arr[i],即交换'
if(minIndex != i){
arr[minIndex] = arr[i];
arr[i] = min;
}
}
System.out.println(Arrays.toString(arr));
}
}
# 选择排序速度比冒泡排序快
插入排序:
# 介绍:
插入式排序属于内部排序法,是对于欲排序的元素以插入的方式寻找该元素的适当位置,以达到排序的目的.
# 思想:
把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有n-1个元素,排序过程中每次从无序表取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表.
### 代码实现:
public class InsertSort {
public static void main(String[] args) {
int[] arr = {119, 1, 34, 101, -1};
for (int i = 1; i < arr.length; i++) {
// 定义待插入的数
int insertVal = arr[i];
int insertIndex = i - 1;
// 1. insertIndex >= 0 保证在给 insertVal找插入位置,不越界
// 2. insertVal < arr[insertIndex] 待插入的数,还没有找到插入位置
// 3. 就需要将 arr[insertIndex] 后移
while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
arr[insertIndex + 1] = arr[insertIndex];
insertIndex--;
}
arr[insertIndex + 1] = insertVal;
}
System.out.println(Arrays.toString(arr));
}
}
希尔排序:
# 介绍:
希尔排序(Shellsort)它是简单插入排序经过改进之后的一个更高效版本,也称为缩小增量排序.时间复杂度O(n^(1.3—2))
# 基本思想:
把记录按下标的一定增量分组,对每组使用直接插入排序算法排序.随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止.
### 代码实现:
## 交换法:
public static void main(String[] args) {
// 希尔排序 交换法
int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
int temp = 0;
int count = 0;
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
for (int i = gap; i < arr.length; i++) {
// 遍历各组中所有的元素(共gap组)
for (int j = i - gap; j >= 0 ; j -= gap) {
if(arr[j] > arr[j + gap]){
temp = arr[j];
arr[j] = arr[j + gap];
arr[j + gap] = temp;
}
}
}
System.out.println("希尔排序第" + (++count) + "轮 = " + Arrays.toString(arr));
}
}
## 移动法:(高效)
public static void main(String[] args) {
// 希尔排序 交换法
int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
// 移动法
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
for (int i = gap; i < arr.length; i++) {
int j = i;
int temp = arr[j];
if(arr[j] < arr[j - gap]){
while (j - gap >= 0 && temp < arr[j - gap]){
// 移动
arr[j] = arr[j - gap];
j -= gap;
}
// 当while退出后,就给temp找到插入的位置
arr[j] = temp;
}
}
}
System.out.println(Arrays.toString(arr));
}
## 移动法比交换法高效!!
快速排序:
# 介绍:
快速排序(Quicksort)是对冒泡排序的一种改进.
# 基本思想:
通过一趟排序 将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列.
### 代码实现
public class QuickSort {
public static void main(String[] args) {
int[] arr = {-9, 78, 0, -57, 23, -57, 70, 70};
quickSort(arr, 0, arr.length - 1);
System.out.println("排序后:");
System.out.println(Arrays.toString(arr));
}
private static void quickSort(int[] arr, int low, int high) {
if (low < high) {
// 找寻基准数据的正确索引
int index = getIndex(arr, low, high);
// 进行迭代对index之前和之后的数组进行相同的操作使整个数组变成有序
//quickSort(arr, 0, index - 1); 之前的版本,这种姿势有很大的性能问题,谢谢大家的建议
quickSort(arr, low, index - 1);
quickSort(arr, index + 1, high);
}
}
private static int getIndex(int[] arr, int low, int high) {
// 基准数据
int tmp = arr[low];
while (low < high) {
// 当队尾的元素大于等于基准数据时,向前挪动high指针
while (low < high && arr[high] >= tmp) {
high--;
}
// 如果队尾元素小于tmp了,需要将其赋值给low
arr[low] = arr[high];
// 当队首元素小于等于tmp时,向前挪动low指针
while (low < high && arr[low] <= tmp) {
low++;
}
// 当队首元素大于tmp时,需要将其赋值给high
arr[high] = arr[low];
}
// 跳出循环时low和high相等,此时的low或high就是tmp的正确索引位置
// 由原理部分可以很清楚的知道low位置的值并不是tmp,所以需要将tmp赋值给arr[low]
arr[low] = tmp;
return low; // 返回tmp的正确位置
}
}
归并排序:
# 介绍:O(n)
归并排序(Merge Sort)是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。时间复杂度O(n log n)
### 代码实现
public class MergeSort {
public static void main(String []args){
int[] arr = new int[8000000];
for (int i = 0; i < 8000000 ; i++) {
arr[i] = new Random().nextInt(8000000);
}
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
System.out.println(df.format(new Date()));// new Date()为获取当前系统时间
// int []arr = {9,8,7,6,5,4,3,2,1};
int[] newArrs = mergeSort(arr, 0, arr.length - 1);
// System.out.println(Arrays.toString(newArrs));
System.out.println(df.format(new Date()));// new Date()为获取当前系统时间
}
public static int[] mergeSort(int[] nums, int l, int h) {
if (l == h)
return new int[] { nums[l] };
int mid = l + (h - l) / 2;
int[] leftArr = mergeSort(nums, l, mid); //左有序数组
int[] rightArr = mergeSort(nums, mid + 1, h); //右有序数组
int[] newNum = new int[leftArr.length + rightArr.length]; //新有序数组
int m = 0, i = 0, j = 0;
while (i < leftArr.length && j < rightArr.length) {
newNum[m++] = leftArr[i] < rightArr[j] ? leftArr[i++] : rightArr[j++];
}
while (i < leftArr.length)
newNum[m++] = leftArr[i++];
while (j < rightArr.length)
newNum[m++] = rightArr[j++];
return newNum;
}
}
基数排序:
# 介绍:
基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog(r)m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。
# 基本思想
将所有待比较数值统一为同样的数位长度,数位较短的前面补零.然后,从最低位开始,依次进行一次排序,这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列.
# 说明:
1. 基数排序是对传统桶排序的扩展,速度很快.
2. 空间换时间,占用内存很大,当对海量数据排序时,容易造成 OutOfMemoryError.
3. 稳定.
### 代码实现
public class RadixSort {
public static void main(String[] args) {
// # 目前的基数排序只支持正整数排序
int []arr = {999, 88, 71, 6, 500, 4, 3, 25, 1, 30};
int max = arr[0];
// 获得最大数
for (int i : arr) {
if(i > max){
max = arr[i];
}
}
// 获得最大数的位数
int maxLength = (max + "").length();
int [][]bucket = new int[10][arr.length];
int []bucketElementCounts = new int[10];
for (int i = 0, n = 1; i < maxLength; n *= 10, i++) {
// 第 i 轮(针对每个元素的对应位进行排序处理)
for (int j = 0; j < arr.length; j++) {
// 取出每个元素的对应位的值
int digitOfElement = arr[j] / n % 10;
// 放入到对应的桶中
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
bucketElementCounts[digitOfElement]++;
}
// 按照这个桶的顺序(一维数组的下标依次取出数据,放到原来的数组)
int index = 0;
// 遍历每一桶,并将桶中的数据放入到原数组
for (int k = 0; k < bucketElementCounts.length; k++) {
// 如果桶中,有数据就放入原数组
if(bucketElementCounts[k] != 0){
// 循环该桶,即第K个桶,放入
for (int l = 0; l < bucketElementCounts[k]; l++) {
// 取出元素放入到arr
arr[index++] = bucket[k][l];
}
}
// 特别注意:一定要重新置为 0
bucketElementCounts[k] = 0;
}
}
System.out.println(Arrays.toString(arr));
}
}
排序算法复杂度比较:
排序算法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 排序方式 | 稳定性 |
---|---|---|---|---|---|---|
冒泡 | O(n2) | O(n2) | O(n2) | O(1) | In-place | 稳定 |
选择 | O(n2) | O(n2) | O(n2) | O(1) | In-place | 不稳定 |
插入 | O(n2) | O(n2) | O(n2) | O(1) | In-place | 稳定 |
希尔 | O(nlogn) | O(nlog2n) | O(nlog2n) | O(1) | In-place | 不稳定 |
归并 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | Out-place | 稳定 |
快速 | O(nlogn) | O(nlogn) | O(n2) | O(logn) | In-place | 不稳定 |
堆 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | In-place | 不稳定 |
计数 | O(n + k) | O(n + k) | O(n + k) | O(k) | Out-place | 稳定 |
桶 | O(n + k) | O(n + k) | O(n2) | O(n + k) | Out-place | 稳定 |
基数 | O(n + k) | O(n + k) | O(n + k) | O(n + k) | Out-place | 稳定 |
# 相关术语解释:
1. 稳定:如果a原本在b前面,而a==b,排序之后a仍在b的前面;
2. 不稳定:如果a原本在b前面,而a==b,排序之后a可能出现在b的后面;
3. 内排序:所有排序操作都在内存中完成;
4. 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
5. 时间复杂度:一个算法执行所耗费的时间;
6. 空间复杂度:运行完一个程序所需内存的大小;
7. n:数据规模;
8. k:"桶"的个数;
9. In-place:不占用额外内存;
10. Out-place:占用额外内存;
# nlogn 线性对数阶,n2 平方对数阶
查找算法:
# 查用的查找算法:
1. 顺序(线性)查找
2. 二分查找/折半查找
3. 插值查找
4. 斐波那契查找
线性查找:
# 介绍:
顺序查找也称为线形查找,属于 ‘无序’ 查找算法。从数据结构线形表的一端开始,顺序扫描,依次将扫描到的结点关键字与给定值k相比较,若相等则表示查找成功;若扫描结束仍没有找到关键字等于k的结点,表示查找失败。
### 代码实现
public class SeqSearch {
public static void main(String[] args) {
int []arr = {1, 9, -1, 11, 3, 11};
int index = seqSearch(arr, 11);
if(index == -1){
System.out.println("没有找到");
}else {
System.out.println("找到的下标为:" + index);
}
}
public static int seqSearch(int []arr, int value){
for (int i = 0; i < arr.length; i++) {
if(arr[i] ==value){
return i;
}
}
return -1;
}
}
二分查找:
# 介绍:
也称为是折半查找,属于 ‘有序’ 查找算法。用给定值k先与中间结点的关键字比较,中间结点把线形表分成两个子表,若相等则查找成功;若不相等,再根据k与该中间结点关键字的比较结果确定下一步查找哪个子表,这样递归进行,直到查找到或查找结束发现表中没有这样的结点。
### 代码实现
## 使用递归找到符合条件的下标
public static void main(String[] args) {
int []arr = {1, 2, 3, 4, 4, 5, 6, 7};
System.out.println(binarySearch(arr, 0, arr.length - 1, 4));
}
public static int binarySearch(int []arr, int left, int right, int findVal){
// left > right,说明没有找到对应的值
if(left > right){
return -1;
}
int mid = (left + right) / 2;
int midVal = arr[mid];
if(findVal > midVal){// 向右递归
return binarySearch(arr, mid + 1, right, findVal);
}else if(findVal < midVal){// 向左递归
return binarySearch(arr, left, mid - 1, findVal);
}else {
return mid;
}
}
## 使用递归找到符合条件的所有下标
public static void main(String[] args) {
int []arr = {1, 2, 3, 4, 4, 5, 6, 7};
System.out.println(binarySearch2(arr, 0, arr.length - 1, 4));
}
public static ArrayList<Integer> binarySearch2(int []arr, int left, int right, int findVal){
// left > right,说明没有找到对应的值
if(left > right) {
return new ArrayList<Integer>();
}
int mid = (left + right) / 2;
int midVal = arr[mid];
if(findVal > midVal){// 向右递归
return binarySearch2(arr, mid + 1, right, findVal);
}else if(findVal < midVal){// 向左递归
return binarySearch2(arr, left, mid - 1, findVal);
}else {
ArrayList<Integer> list = new ArrayList<>();
list.add(mid);
int temp = mid - 1;
while (true) {// 向左找
if(temp < 0 || arr[temp] != findVal){
break;
}
list.add(temp--);
}
temp = mid + 1;
while (true) {// 向右找
if(temp > arr.length-1 || arr[temp] != findVal){
break;
}
list.add(temp++);
}
return list;
}
}
插值查找:
# 介绍:
基于二分查找算法,将查找点的选择改进为自适应选择,可以提高查找效率。当然,差值查找也属于 ‘有序’ 查找。
# 注意事项:
1. 对于数据量较大,关键字分布比较均匀的查找表来说,采用插值查找,速度较快.
2. 关键字分布不均匀的情况下,该方法不一定比折半查找要好.
# 原理对比:
key:需要查找的值.
二分查找中查找点计算如下:
mid=(low+high)/2, 即mid=low+1/2*(high-low);
通过类比,我们可以将查找的点改进为如下:
mid=low+((key-arr[low])/(arr[high]-arr[low]))*(high-low)
### 代码实现
public class InsertValueSearch {
public static void main(String[] args) {
int []arr = {1, 2, 3, 4, 4, 5, 6, 7};
System.out.println(insertValueSearch(arr, 0, arr.length - 1, 1));
}
public static int insertValueSearch(int []arr, int left, int right, int findVal){
System.out.println("插值算法:");
// left > right,说明没有找到对应的值
if(left > right || findVal < arr[0] || findVal > arr[arr.length - 1]){
return -1;
}
int mid = left + ((findVal - arr[left]) / (arr[right] - arr[left])) * (right - left);
int midVal = arr[mid];
if(findVal > midVal){// 向右递归
return insertValueSearch(arr, mid + 1, right, findVal);
}else if(findVal < midVal){// 向左递归
return insertValueSearch(arr, left, mid - 1, findVal);
}else {
return mid;
}
}
}
斐波那契查找:
# 介绍:
也是二分查找的一种提升算法,通过运用黄金比例的概念在数列中选择查找点进行查找,提高查找效率。同样地,斐波那契查找也属于一种 ‘有序’ 查找算法。
# 斐波那契算法基本介绍:
1.黄金分割点是把一条线段分割为两部分,是其中一部分与全长之比等于另一部分与这部分之比,取其前三位数的近似值为0.618。由于按此比例设计的造型十分美丽,因此称为黄金分割
2.斐波那契数列{1,1,2,3,5,8,13,21,34,55}发现斐波那契数列的相邻两个数的比例,无限接近黄金分格值0.618
3.斐波那契工作原理:斐波那契查找与二分查找和插入查找原理非常相似,仅仅改变了中间节点(mid)的位置,mid不在是中间或者是插值得到,而是位于黄金分割点附近,即mid=low+F(k-1)-1(F代表斐波那契数列)
# 对F(k-1)-1的理解:
1.通过斐波那契 数列F[k]=F[k-1]+F[k-2] 的性质,可以得到(F[k]-1)=(F[k-1]-1)+(F[k-2]-1)+1
该式说明:只要顺序表的长度为F[k]-1,则可以将表分为长度为F[k-1]-1和F[k-2]-1的两端。从而中间位置为 mid=low=F(k-1)-1
2.类似的每个子段也可以使用相同的方式分割
3.但是顺序表的长度n不一定刚好等于F[k]-1,所以需要将原来的顺序表长度n增加至F[k]-1。这里的k值只要能 使得F[k]-1恰好大于或等于n即可
### 代码实现
public class FibonacciSearch {
public static int maxSize = 20;// 定义斐波那契数组大小
public static void main(String[] args) {
int []arr = {1, 2, 3, 4, 5, 6, 7};
int index = fibonacciSearch(arr, 7);
System.out.println(index);
}
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 fibonacciSearch(int[] arr, int searchValue){
int low = 0;
int high = arr.length - 1;
int k = 0;// 表示斐波那契分隔值的下标
int[] f = fib();// 获取斐波那契数组
while (high > f[k] - 1) {
k++;
}
// 创建临时数组,因为f[k]值可能大于a的长度,因此需要使用Arrays工具类,构造一个新法数组,并指向temp[],不足的部分会使用0补齐
int []temp = Arrays.copyOf(arr, f[k]);
// 将临时数组的值填充,实际需要使用arr数组的最后一个数来填充不足的部分
for (int i = high + 1; i < temp.length; i++) {
temp[i] = high;
}
while (low <= high){
int mid = low + f[k - 1] - 1;// 斐波那契黄金分割值
if(searchValue > temp[mid]){
low = mid + 1;
/**
* 对k-=2理解
* 1.全部元素=前面的元素+后面的元素
* 2.f[k]=k[k-1]+f[k-2]
* 3.因为后面有k-2个元素,所以可以继续拆分f[k-2]=f[k-3]+f[k-4]
* 4.即在f[k-2]前面进行查找k-=2
* 5.即在下次循环mid=[k-1-2]-1
*/
k -= 2;
}else if(searchValue < temp[mid]){
high = mid - 1;
/*
* 对k--进行理解
* 1.全部元素=前面的元素+后面的元素
* 2.f[k]=k[k-1]+f[k-2]
* 因为前面有k-1个元素,所以可以继续分为f[k-1]=f[k-2]+f[k-3]
* 即在f[k-1]的前面继续查找k--
* 即下次循环,mid=f[k-1-1]-1
*/
k -= 1;
}else {
if(mid <= high){
return mid;
}else {
return high;
}
}
}
return -1;
}
}
二分查找非递归:
### 代码实现:
// 二分查找法(非递归查找)
public class BinarySearchNoRecur {
public static void main(String[] args) {
int []arr = {1, 3, 8, 11, 67, 100};
System.out.println(binarySearch(arr, 11));
}
/**
*
* @param arr 待查找的数组(数组是有序的)
* @param target 需要查找的数
* @return
*/
public static int binarySearch(int[] arr, int target){
int left = 0;
int right = arr.length - 1;
while (left <= right){
int mid = (left + right) / 2;
if(arr[mid] == target){
return mid;
}else if(arr[mid] > target){
right = mid - 1;// 需要向左查找
}else {
left = mid + 1;// 需要向右查找
}
}
return -1;
}
}
分治算法:
# 介绍:
把一个复杂的问题分成两个或更多的相同或类似的子问题,再把子问题分成更小的子问题...直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序、归并排序)
# 分治算法最佳实践-汉诺塔:
// 分治算法:汉诺塔
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("第1个盘从" + a + "->" + c);
}else {
// 如果我们有n>=2情况,可以看作是两个盘1.最下边的一个盘2.上边所有盘
// 1. 先把最上面的所有盘 A->B,移动过程会使用到C
hanoiTower(num - 1, a, c, b);
// 2. 把最下边的盘 A-> C
System.out.println("第" + num + "个盘从" + a + "->" + c);
// 3. 把B塔所有盘从 B->C,移动过程使用到a塔
hanoiTower(num - 1, b, a, c);
}
}
}
动态规划算法:
# 介绍:
1. 动态规划算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法
2. 它与分治算法类似,其基本思想也是将待求解问题分成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解
3. 与分治算法不同的是,适用于动态规划求解的问题,经分解得到子问题往往不是相互独立的。(即下一个子阶段的求解建立在上一个子阶段的解的基础上)
4. 动态规划可以通过填表的方式来逐步推进,得到最优解
# 应用场景-背包问题:
1. 要求达到的目标为装入背包的总价值最大,并且重量不超出
2. 不能装入相同物品
后续继续更新…