一、概述
程序 = 数据结构 + 算法
1.1 数据结构包括:线性结构和非线性结构
1.1.1 线性结构
- 线性结构作为最常用的数据结构,其特点是数据元素之间存在一对一的线性关系
- 线性结构有两种不同的存储结构,即 顺序存储结构( 数组)和 链式存储结构( 链表)
- 顺序存储的线性表称为顺序表,顺序表中的 存储元素是连续的;链式存储的线性表称为链表,链表中的 存储元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息
- 线性结构常见的有: 数组、队列、链表和栈
1.1.2 非线性结构
非线性结构包括:二维数组,多维数组,广义表, 树结构,图结构
二、稀疏数组和队列
2.1 稀疏 sparsearray 数组
2.1.1 基本介绍
当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组。
稀疏数组的处理方法是:
- 记录数组 一共有几行几列,有多少个不同的值
- 把具有不同值的元素的行列及值记录在一个小规模的数组中,从而 缩小程序的规模
2.1.2 应用案例
2.1.3 代码实现
public class SparseArray {
public static void main(String[] args) {
//创建原始二维数组11*11
//0:没有棋子 1:黑 2:蓝
int chessArr1[][] = new int[11][11];
chessArr1[1][2] = 1;
chessArr1[2][3] = 2;
chessArr1[5][6] = 10;
System.out.println("原始二维数组:");
for (int[] row : chessArr1) {
for (int data : row) {
System.out.printf("%d\t",data);
}
System.out.println();
}
//转为稀疏数组
//1.遍历,得到非零数据的个数
int sum = 0;
for (int i = 0; i < chessArr1.length; i++) {//行数
for (int j = 0; j < chessArr1[i].length; 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 < chessArr1.length; i++) {//行数
for (int j = 0; j < chessArr1[i].length; j++) {//列数
if(chessArr1[i][j]!=0){
count++;
sparseArr[count][0] = i;//该行的第1列放二维数组中的【列】
sparseArr[count][1] = j;//该行的第2列放二维数组中的【行】
sparseArr[count][2] = chessArr1[i][j];//该行的第3列【放值】
}
}
}
System.out.println();
System.out.println("得到的稀疏数组:");
for (int i = 0; i < sparseArr.length; i++) {
System.out.printf("%d\t%d\t%d\t",sparseArr[i][0],sparseArr[i][1],sparseArr[i][2]);
System.out.println();
}
//3.将稀疏数组恢复成原始的二维数组
//3.1读取稀疏数组第一行创建原始二维数组
int chessArr2[][] = new int[sparseArr[0][0]][sparseArr[0][1]];
//3.2给二维数组赋值
for (int i = 1; i < sparseArr.length; i++) {//稀疏数组第1行的值已经在创建数组的时候使用
chessArr2[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][2];
}
System.out.println("恢复后的二维数组:");
for (int[] row : chessArr2) {
for (int data : row) {
System.out.printf("%d\t",data);
}
System.out.println();
}
}
}
2.2 队列
2.2.1 基本介绍
- 队列是一个 有序列表,可以用 数组或是 链表来实现。
- 遵循 先入先出的原则。即: 先存入队列的数据,要先取出。后存入的要后取出
2.2.2 数组模拟队列
- 队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如下图, 其中 maxSize 是该队列的最大容量。
- 因为队列的输出、输入是分别从前后端来处理,因此需要两个变量 front 及 rear 分别记录队列前后端的下标,front 会随着数据输出而改变,而 rear 则是随着数据输入而改变,如图所示:
- 将数据存入队列时称为”addQueue”,addQueue 的处理需要有两个步骤:
- 将尾指针往后移:rear+1 , 当 front == rear 表示队列【空】
- 若尾指针 rear 小于队列的最大下标 maxSize-1,则将数据存入 rear 所指的数组元素中,否则无法存入数据。rear == maxSize - 1表示【队列满】
2.2.3 数组模拟队列代码实现
public class ArrayQueue {
private int maxSize;//数组最大容量
private int front;//队列头
private int rear;//队列尾
private int[] arr;//该数组用于存放数据,模拟队列
//创建队列
public ArrayQueue(int arrMaxSize){
maxSize = arrMaxSize;
arr = new int[maxSize];
front = -1;//指向队列头的前一个位置
rear = -1;//指向队列尾(也可以理解为当前刚添加进去的元素)
}
//判断队列是否为满
public boolean isFull(){
return rear == maxSize-1;
}
//判断队列是否为空
public boolean isEmpty(){
return rear == front;
}
//添加数据到队列
public void addQueue(int n){
//判断是否为满
if(isFull()){
System.out.println("queue was fulled!");
return;
}
rear++;//rear后移
arr[rear] = n;//添加数据 arr[++rear] = n
}
//获取队列数据,出队列
public int getQueue(){
//判断是否为空
if(isEmpty()){
//通过抛出异常
throw new RuntimeException("queue is empty! do not get data!");
}
front++;//front后移
return arr[front];//return arr[++front]
}
//显示队列所有数据
public void showQueue(){
if(isEmpty()){
System.out.println("queue is empty! no data!");
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("queue is empty! do not get data!");
}
return arr[front+1];
}
}
import java.util.Scanner;
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("e(exit):退出程序");
System.out.println("a(add):添加数据");
System.out.println("g(get):取出数据");
System.out.println("h(head):队头数据");
key = scanner.next().charAt(0);//接收一个字符(一个字符串的第一个字符)
switch (key){
case 's':
arrayQueue.showQueue();
break;
case 'a':
System.out.println("请输入一个数字:");
arrayQueue.addQueue(scanner.nextInt());
break;
case 'g':
try {
System.out.printf("取出的数据是:%d\n",arrayQueue.getQueue());
}catch(Exception e){
System.out.println(e.getMessage());
}
break;
case 'h':
try{
System.out.printf("队头数据是:%d\n",arrayQueue.headQueue());
}catch(Exception e){
System.out.println(e.getMessage());
}
break;
case 'e':
scanner.close();
loop = false;
break;
default:
break;
}
}
System.out.println("exit!");
}
}
上述代码存在问题分析并优化:
- 目前数组使用一次就不能用, 没有达到复用的效果
- 将这个数组使用算法,改进成一个列 环形的队列 ,取模:%
2.2.4 数组模拟环形队列
对前面的数组模拟队列的优化,充分利用数组. 因此将数组看做是一个环形的。(通过取余的方式来实现即可)
分析说明:
- 尾索引的下一个为头索引时表示队列满,即将队列容量空出一个作为约定,这个在做判断队列满的时候需要注意 (rear + 1) % maxSize == front 【满】
- rear == front 【空】
2.2.5 数组模拟环形队列代码实现
public class CircleArrayQueue {
private int maxSize;//数组最大容量
private int front;//队列头,初始值为0,指向队列的第一个元素
private int rear;//队列尾,初始值为0,指向队列最后一个元素的后一个位置,空出一个位置【判断队列状态】
private int[] arr;//该数组用于存放数据,模拟队列
//创建队列
public CircleArrayQueue(int arrMaxSize){
maxSize = arrMaxSize;
arr = new int[maxSize];
}
//判断队列是否为满
public boolean isFull(){
return front == (rear+1)%maxSize;
}
//判断队列是否为空
public boolean isEmpty(){
return rear == front;
}
//添加数据到队列
public void addQueue(int n){
//判断是否为满
if(isFull()){
System.out.println("queue was fulled!");
return;
}
arr[rear] = n;//添加数据
rear = (rear + 1) % maxSize;//防止数组越界
}
//获取队列数据,出队列
public int getQueue(){
//判断是否为空
if(isEmpty()){
//通过抛出异常
throw new RuntimeException("queue is empty! do not get data!");
}
int data = arr[front];
front = (front + 1) % maxSize;
return data;
}
//求出当前队列有效数据的个数
public int size(){
return (rear + maxSize -front) % maxSize;
}
//显示队列所有数据
public void showQueue(){
if(isEmpty()){
System.out.println("queue is empty! no data!");
return;
}
//从front开始遍历,遍历front + size()个数据。取模都是为了防止数组越界
for (int i = front ; i < front + size(); i++) {
System.out.printf("arr[%d]=%d\n",i % maxSize,arr[i % maxSize]);
}
}
//显示队列的头数据。注意:不是取出数据
public int headQueue(){
if(isEmpty()){
throw new RuntimeException("queue is empty! do not get data!");
}
return arr[front];
}
}
import java.util.Scanner;
public class CircleArrayQueueDemo {
public static void main(String[] args) {
//创建一个环形队列
CircleArrayQueue arrayQueue = new CircleArrayQueue(4);//队列有效数据个数为3
char key = ' ';//接收用户输入
Scanner scanner = new Scanner(System.in);
boolean loop = true;
while(loop){
System.out.println("s(show):显示队列");
System.out.println("e(exit):退出程序");
System.out.println("a(add):添加数据");
System.out.println("g(get):取出数据");
System.out.println("h(head):队头数据");
key = scanner.next().charAt(0);//接收一个字符(一个字符串的第一个字符)
switch (key){
case 's':
arrayQueue.showQueue();
break;
case 'a':
System.out.println("请输入一个数字:");
arrayQueue.addQueue(scanner.nextInt());
break;
case 'g':
try {
System.out.printf("取出的数据是:%d\n",arrayQueue.getQueue());
}catch(Exception e){
System.out.println(e.getMessage());
}
break;
case 'h':
try{
System.out.printf("队头数据是:%d\n",arrayQueue.headQueue());
}catch(Exception e){
System.out.println(e.getMessage());
}
break;
case 'e':
scanner.close();
loop = false;
break;
default:
break;
}
}
System.out.println("exit!");
}
}
三、链表
3.1 链表介绍
链表是有序的列表,但是其在内存中的存储如下:
- 链表是以节点的方式来存储, 是链式存储
- 每个节点包含 data 域, next 域:指向下一个节点.
- 如图:发现 链表的各个节点不一定是连续存储.
- 链表分 带头节点的链表和 没有头节点的链表,根据实际的需求来确定
单链表(带头结点) 逻辑结构示意图如下:
3.2 单链表应用实例
使用带 head 头的单向链表实现 –英雄排行榜管理完成对英雄人物的增删改查操作。
- 第一种方法在添加英雄时,直接添加到链表的尾部
- 第二种方式在添加英雄时, 根据排名将英雄插入到指定位置(如果有这个排名,则添加失败)
- 修改节点功能 (1) 先找到该节点,通过遍历 (2) temp.name = newHeroNode.name ; temp.nickname= newHeroNode.nickname
- 删除节点
5.获取单链表中有效节点的个数
6.查找单链表中的倒数第 k 个结点
7.单链表的反转【有点难度】
8.从尾到头打印单链表
- 可以在7反转的基础上直接打印(会破坏原链表的结构)
- 反向遍历,使用Stack栈
3.3 单链表应用实例代码实现
public class HeroNode {
//每个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;
}
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
// ", next=" + next +
'}';
}
}
public class SingleLinkedList {
//管理HeroNode
//定义头结点,头结点不变,不存放具体的数据
private HeroNode head = new HeroNode(0,"","");
//返回头结点
public HeroNode getHead() {
return head;
}
//添加节点到单链表
/*
* 不考虑编号顺序时:
* 1.找到当前链表的最后节点
* 2.将最后一个节点的next指向新的节点
* */
public void add(HeroNode heroNode){
//head节点不能动,需要定义一个辅助变量 temp
HeroNode temp = head;
//遍历链表,找到链表的最后一个节点
while(true){
if(temp.next == null){//链表最后一个节点的next为null
break;
}
//如果没有找到最后,将temp后移
temp = temp.next;
}
//当退出while循环时,temp已经指向链表最后了。
//将最后这个节点的next指向新的节点
temp.next = heroNode;
}
//添加节点到单链表
/*
* 【考虑】编号顺序时:
* 根据no将英雄插入到指定的位置(若no存在,则插入失败)
* */
public void addByOrder(HeroNode heroNode){
//head节点不能动,任然需要定义一个辅助变量 temp
//因为是单链表,所以temp位于【添加位置】的前一个节点
HeroNode temp = head;
boolean flag = false;//表示添加的no是否存在,默认为false
while(true){
if(temp.next==null){//temp已经在链表的末尾
break;
}
if(temp.next.no > heroNode.no){//找到插入位置,就在temp后插入
break;
}else if(temp.next.no == heroNode.no){//待添加的heroNode的编号已然存在
flag = true;
break;
}
temp = temp.next;//如果上面三个if都不成立,则后移,继续往后遍历当前链表
}
//判断flag
if(flag){//no已经存在,不能添加
System.out.printf("编号%d已经存在,添加失败!",heroNode.no);
}else{//在temp的后面插入新节点heroNode
heroNode.next = temp.next;
temp.next = heroNode;
}
}
//根据no修改链表
public void update(HeroNode newHeroNode){
if(head.next == null){
System.out.println("链表空,不能修改!");
return;
}
//head节点不能动,任然需要定义一个辅助变量 temp
HeroNode temp = head;
boolean flag = false;//表示是否找到该节点
while(true){
if(temp == null){
break;//已经遍历完链表
}
if(temp.no == newHeroNode.no){
flag = true;
break;
}
temp = temp.next;//上面两个if都不满足,继续往后遍历链表
}
//判断flag
if(flag){
temp.name = newHeroNode.name;
temp.nickname = newHeroNode.nickname;
}else{
System.out.printf("没有找到编号为%d的节点,不能修改!",newHeroNode.no);
}
}
//删除节点
public void delete(HeroNode deleteHeroNode){
if(head.next == null){
System.out.println("链表空,不能修改!");
return;
}
//head节点不能动,任然需要定义一个辅助变量 temp
HeroNode temp = head;//需要的待删除节点的前一个节点
boolean flag = false;//表示是否找到该节点的前一个节点
while(true) {
if (temp.next == null) {
break;//已经遍历完链表
}
if(temp.next.no == deleteHeroNode.no){//找到了待删除节点的前一个节点
flag= true;
break;
}
temp = temp.next;//不满足以上两个if则继续遍历链表
}
//判断flag
if(flag){
temp.next = temp.next.next;
}else{
System.out.printf("%d号元素不存在,删除失败!",deleteHeroNode.no);
}
}
//显示链表【遍历】
public void list(){
//判断链表是否为空
if(head.next == null){
System.out.println("list is empty!");
return;
}
//head节点不能动,需要定义一个辅助变量 temp
HeroNode temp = head.next;
while(true){
if(temp == null){//判断是否到链表最后
break;
}
System.out.println(temp);//输出节点信息
//temp后移【一定记住】
temp = temp.next;
}
}
//获取单链表有效节点的个数
public int getLehgth(HeroNode head){
int length = 0;
//定义一个辅助变量
HeroNode curNode = head.next;
while(curNode != null){
length++;
curNode = curNode.next;//遍历
}
return length;
}
//查找单链表中倒数第k个节点
/*
* 1.参数为head,和index = k
* 2.调用getLength()方法遍历链表得到链表的长度size
* 3.从头开始遍历链表到第(size-index)个,就得到倒数第k个节点
* 4.如果找到了返回该节点,否则返回null
* */
public HeroNode findLastKNode(HeroNode head, int index){
if(head.next == null){
return null;//链表为空直接返回null
}
int size = getLehgth(head);
//判断index合法性
if(index <= 0||index > size){
return null;
}
//定义辅助变量
HeroNode curCode = head.next;
//继续遍历到size-index的位置
for(int i = 0;i<size-index;i++){
curCode = curCode.next;
}
return curCode;
}
//链表反转
public void reverseList(){
if(head.next == null || head.next.next == null){
return ;//链表为空或者只有一个节点,不用反转
}
HeroNode curNode = head.next;//定义辅助变量,遍历原来链表
HeroNode temp = null;//定义辅助变量,指向当前节点curNode的下一个节点
HeroNode reverseHead = new HeroNode(0,"","");//反转后的新链表的头结点
//遍历原来链表,每遍历到一个节点,取出放到新链表reverseHead的最前端
while(curNode != null){
temp = curNode.next;//暂时保存当前节点的下一个节点
curNode.next = reverseHead.next;//让当前节点curNode指向反转链表头节点的下一个 节点
reverseHead.next = curNode;//遍历到的当前节点加入到反转链表头节点的下一个节点
/*
* 【1】 curNode.next = reverseHead.next;
* 【2】 reverseHead.next = curNode;
* 【当前节点插入到反转后新链表的头结点与第一个有节点之间】
* 当前遍历到的节点指向反转链表头节点的下一个节点(让新插入的节点指向前一个插入的节点)
* 反转后新链表的头结点指向当前遍历到的节点
*
* 例如:原来链表:head->1->2->3 反转链表:reverseHead->null
* 【1】 1->null; 2->1; 3->2;
* 【2】 reverseHead->1; reverseHead->2 reverseHead->3;
* */
curNode = temp;//当前节点已经插入到新链表的头部,当前节点的下一个节点置为当前节点
}
//将head.next指向reverseHead.next,实现单链表的反转
head.next = reverseHead.next;
}
//逆序打印(使用栈)
public void reversePrint(){
if(head.next==null){
return;//空链表
}
//创建栈,将各个节点压入栈
Stack<HeroNode> heroNodeStack = new Stack<HeroNode>();
HeroNode cur = head.next;
while(cur!=null){
heroNodeStack.push(cur);//push()压入栈【和add()一样】
cur = cur.next;//后移
}
//依次出栈
while(!heroNodeStack.isEmpty()){
System.out.println(heroNodeStack.pop());//先进后出,实现逆序打印
}
}
}
public class SingleLinkedListDemo {
public static void main(String[] args) {
HeroNode hero1 = new HeroNode(1, "zt", "前一姐");
HeroNode hero2 = new HeroNode(2, "dq", "现一姐");
HeroNode hero3 = new HeroNode(3, "lss", "一姐准备");
HeroNode hero4 = new HeroNode(4, "wbb", "流量密码");
//创建链表
SingleLinkedList singleLinkedList = new SingleLinkedList();
//加入
// singleLinkedList.add(hero1);
// singleLinkedList.add(hero3);
// singleLinkedList.add(hero2);
// singleLinkedList.add(hero4);
//按照no顺序加入
singleLinkedList.addByOrder(hero1);
singleLinkedList.addByOrder(hero3);
singleLinkedList.addByOrder(hero4);
singleLinkedList.addByOrder(hero2);
singleLinkedList.list();
//修改no为1的节点的name为ztt
System.out.println();
HeroNode hero5 = new HeroNode(1, "ztt", "前一姐");
singleLinkedList.update(hero5);
singleLinkedList.list();
//删除no=3的hereNode
System.out.println();
singleLinkedList.delete(hero3);
singleLinkedList.list();
//当前链表节点个数
HeroNode headNode = singleLinkedList.getHead();
int lehgth = singleLinkedList.getLehgth(headNode);
System.out.printf("当前链表节点数为%d",lehgth);
//倒数第二个节点
HeroNode lastKNode = singleLinkedList.findLastKNode(headNode, 2);
System.out.println("倒数第2个节点是"+lastKNode);
//链表反转
singleLinkedList.reverseList();
singleLinkedList.list();
//逆序打印(使用栈)
singleLinkedList.reversePrint();
}
}
3.4 双向链表应用实例及实现
3.4.1 双向链表的操作分析
使用带 head 头的双向链表实现 –——英雄排行榜
单向链表的缺点:
- 单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。
- 单向链表不能自我删除,需要靠辅助节点 ,而双向链表,则可以自我删除。单链表删除节点时,总是找到 temp,temp 是待删除节点的前一个节点。
对上图的说明:
1. 遍历方法和 单链表一样,只是可以向前,也可以向后查找
2. 添加 (默认添加到双向链表的最后)
(1) 先找到双向链表的最后这个节点
(2) temp.next = newHeroNode
(3) newHeroNode.pre = temp;
3. 修改 思路和 原来的单向链表一样.
4. 删除
(1) 因为是双向链表,可以实现自我删除某个节点
(2) 直接找到要删除的这个节点,比如 temp
(3) temp.pre.next = temp.next;
(4) temp.next.pre = temp.pre;
3.4.2 代码实现
public class HeroNode2 {
public int no;
public String name;
public String nickname;
public HeroNode2 next;//指向下一个节点,默认为null
public HeroNode2 pre;//指向前一个节点,默认为null
public HeroNode2(int no, String name, String nickname) {
this.no = no;
this.name = name;
this.nickname = nickname;
}
@Override
public String toString() {
return "HeroNode2{" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
public class DoubleLinkedList {
//定义头结点,头结点不变,不存放具体的数据
private HeroNode2 head = new HeroNode2(0,"","");
//返回头结点
public HeroNode2 getHead() {
return head;
}
//遍历双向链表
public void list(){
//判断链表是否为空
if(head.next == null){
System.out.println("list is empty!");
return;
}
//head节点不能动,需要定义一个辅助变量 temp
HeroNode2 temp = head.next;
while(true){
if(temp == null){//判断是否到链表最后
break;
}
System.out.println(temp);//输出节点信息
//temp后移【一定记住】
temp = temp.next;
}
}
//添加(默认添加到双向链表的最后)
public void add(HeroNode2 heroNode){
//head节点不能动,需要定义一个辅助变量 temp
HeroNode2 temp = head;
//遍历链表,找到链表的最后一个节点
while(true){
if(temp.next == null){//链表最后一个节点的next为null
break;
}
//如果没有找到最后,将temp后移
temp = temp.next;
}
//当退出while循环时,temp已经指向链表最后了。
//将最后这个节点的next指向新的节点,形成一个双向链表
temp.next = heroNode;
heroNode.pre = temp;
}
//添加(根据no顺序添加)
public void addByOrder(HeroNode2 heroNode){
//head节点不能动,任然需要定义一个辅助变量 temp
//temp位于【添加位置】的前一个节点
HeroNode2 temp = head;
boolean flag = false;//表示添加的标号是否存在,默认为false
while(true){
if(temp.next==null){//temp已经在链表的末尾
break;
}
if(temp.next.no > heroNode.no){//找到插入位置,就在temp后插入
break;
}else if(temp.next.no == heroNode.no){//待添加的heroNode的编号已然存在
flag = true;
break;
}
temp = temp.next;//如果上面三个if都不成立,则后移,继续往后遍历当前链表
}
//判断flag
if(flag){//no已经存在,不能添加
System.out.printf("编号%d已经存在,添加失败!",heroNode.no);
}else{//在temp的后面插入新节点heroNode
heroNode.next = temp.next;
temp.next = heroNode;
if(temp.next!=null){
temp.next.pre = heroNode;
}
}
}
//修改一个节点的内容
public void update(HeroNode2 newHeroNode){
if(head.next == null){
System.out.println("链表空,不能修改!");
return;
}
//head节点不能动,任然需要定义一个辅助变量 temp
HeroNode2 temp = head;
boolean flag = false;//表示是否找到该节点
while(true){
if(temp == null){
break;//已经遍历完链表
}
if(temp.no == newHeroNode.no){
flag = true;
break;
}
temp = temp.next;//上面两个if都不满足,继续往后遍历链表
}
//判断flag
if(flag){
temp.name = newHeroNode.name;
temp.nickname = newHeroNode.nickname;
}else{
System.out.printf("没有找到编号为%d的节点,不能修改!",newHeroNode.no);
}
}
//删除
/*
* 对于双向链表,可以直接找到待删除的节点
*
* */
public void delete(HeroNode2 deleteHeroNode){
if(head.next == null){
System.out.println("链表空,不能修改!");
return;
}
//head节点不能动,任然需要定义一个辅助变量 temp
HeroNode2 temp = head.next;//待删除节点为当前节点
boolean flag = false;//表示是否找到该待删除节点
while(true) {
if (temp == null) {
break;//已经遍历完链表
}
if(temp.no == deleteHeroNode.no){//找到了待删除节点
flag= true;
break;
}
temp = temp.next;//不满足以上两个if则继续遍历链表
}
//判断flag
if(flag){//找到待删除节点
temp.pre.next = temp.next;//待删除节点的前一个节点指向待删除节点的下一个节点
if(temp.next!=null){//如果删除的是最后一个节点,不做判断的话会空指针异常
temp.next.pre = temp.pre;//待删除节点的下一个节点待删除节点的前一个节点
}
}else{
System.out.printf("%d号元素不存在,删除失败!",deleteHeroNode.no);
}
}
}
public class DoubleLinkedListDemo {
public static void main(String[] args) {
System.out.println("双向链表的测试:");
HeroNode2 hero1 = new HeroNode2(1, "zt", "前一姐");
HeroNode2 hero2 = new HeroNode2(2, "dq", "现一姐");
HeroNode2 hero3 = new HeroNode2(3, "lss", "一姐准备");
HeroNode2 hero4 = new HeroNode2(4, "wbb", "流量密码");
DoubleLinkedList doubleLinkedList = new DoubleLinkedList();
doubleLinkedList.add(hero1);
doubleLinkedList.add(hero2);
doubleLinkedList.add(hero3);
doubleLinkedList.add(hero4);
doubleLinkedList.list();
//修改
HeroNode2 hero5 = new HeroNode2(2, "dqq", "现一姐");
doubleLinkedList.update(hero5);
System.out.println("修改:");
doubleLinkedList.list();
//删除
doubleLinkedList.delete(hero3);
System.out.println("删除:");
doubleLinkedList.list();
//按no顺序添加
HeroNode2 hero6 = new HeroNode2(6, "tx", "master");
doubleLinkedList.addByOrder(hero3);
doubleLinkedList.addByOrder(hero6);
System.out.println("按顺序添加:");
doubleLinkedList.list();
}
}
3.5 单向循环链表Josephu(约瑟夫)问题
问题描述:设编号为 1,2,… n 的 n 个人围坐一圈,约定编号为 k(1<=k<=n)的人从 1 开始报数,数到 m 的那个人出列,它的下一位又从 1 开始报数,数到 m 的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
提示:用一个不带头节点的循环链表来处理 Josephu 问题:先构成一个有 n 个结点的单循环链表,然后由 k 节点起从 1 开始计数,计到 m 时,对应节点从链表中删除,然后再从被删除节点的下一个节点又从 1 开始计数,直到最后一个节点从链表中删除算法结束。
图示:
图解1:
图解2:
3.6 Josephu(约瑟夫)问题代码演示
public 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;
}
@Override
public String toString() {
return "Boy{" +
"no=" + no +
'}';
}
}
public class CircleSingleLinkedList {
//创建first节点,当前没有编号
private Boy first = new Boy(-1);
//添加Boy节点,构成一个环形链表
public void addBoy(int nums){
//校验nums
if(nums<1){
System.out.println("nums值错误!");
return;
}
Boy curBoy = null;//辅助变量,帮助构建环形链表
//创建环形链表
for(int i = 1;i<=nums;i++){
Boy boy = new Boy(i);
if(i==1){//如果是第一个小孩
first = boy;
first.setNext(first);//构成环状
curBoy = first;//让辅助变量curBoy指向第一个小孩
}else{
curBoy.setNext(boy);
boy.setNext(first);
curBoy = boy;
}
}
}
//遍历当前环形链表
public void listBoy(){
if(first==null){
System.out.println("链表为空!");
return;
}
Boy curBoy = first;//辅助变量,帮助遍历环形链表
while(true){
System.out.println(curBoy);
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;
while(true){
if(helper.getNext()==first){//helper指向了最后一个节点
break;
}
helper = helper.getNext();
}
//始终让helper在first的后一位
//报数前,先让first和helper移动k-1次(k:从第几个节点开始报数)
for (int i = 0; i < startNo - 1; i++) {
first = first.getNext();
helper = helper.getNext();
}
//报数时,让first和helper移动m-1次(m:数几个数字出圈)
//循环,直到圈中只有一个节点
while(true){
if(helper == first){//圈中只有一个节点
break;
}
//移动first和helper
for (int i = 0; i < countNum - 1; i++) {
first = first.getNext();
helper = helper.getNext();
}
System.out.println("第"+first.getNo()+"个boy出圈");
//first指向的节点出圈
first = first.getNext();
helper.setNext(first) ;
}
System.out.printf("最后留在圈中的是"+first.getNo()+"号boy");
}
}
/*
* 约瑟夫问题:
* Boy
* CircleSingleLinkedList
*
* */
public class Josephu {
public static void main(String[] args) {
CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
circleSingleLinkedList.addBoy(5);
circleSingleLinkedList.listBoy();
circleSingleLinkedList.countBoy(1,3,5);
}
}
四、栈
4.1 栈的概述
- 栈的英文为(stack)
- 栈是一个 先入后出(FILO-First In Last Out) 的有序列表。
- 栈(stack) 是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。 允许插入和删除的一端,为 变化的一端,称为栈顶(Top) ,另一端为 固定的一端,称为栈底(Bottom) 。
- 根据栈的定义可知 , 最先放入栈中元素在栈底 , 最后放入的元素在栈顶 , 而删除元素刚好相反 , 最后放入的元素最先删除,最先放入的元素最后删除。
- 图解方式说明出栈(pop) 和入栈(push)。
4.2 栈的应用场景
- 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。
- 处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆栈中。
- 表达式的转换[中缀表达式转后缀表达式]与求值(实际解决)。
- 二叉树的遍历。
- 图形的深度优先(depth 一 first)搜索法。
4.3 数组模拟栈
栈是一种有序列表,可以使用数组的结构来储存栈的数据内容。
4.3.1 图示
4.3.2 代码实现
public class ArrayStack {
private int maxSize;//栈的大小
private int[] stack;//数组模拟栈
private int top = -1;//top表示栈顶,初始化为-1
//构造器
public ArrayStack(int maxSize) {
this.maxSize = maxSize;
stack = new int[this.maxSize];
}
//栈满
public boolean isFull(){
return top == maxSize - 1;
}
//栈空
public boolean isEmpty(){
return top==-1;
}
//入栈
public void push(int value){
if(isFull()){
System.out.println("栈已满!");
return;
}
stack[++top] = value;
}
//出栈
public int pop(){
if(isEmpty()){
throw new RuntimeException("栈空!");
}
return stack[top--];
}
//遍历栈(从栈顶开始显示数据)
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]);
}
}
}
import java.util.Scanner;
/*
* 测试栈
* */
public class ArrayStackDemo {
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("pop:取出数据(出栈)");
System.out.println("push:添加数据(入栈)");
System.out.println("请输入:");
key = scanner.next();
switch (key){
case"show":
arrayStack.list();
break;
case"push":
System.out.println("请输入一个数:");
arrayStack.push(scanner.nextInt());
break;
case"pop":
try{
System.out.println("出栈数据为:"+arrayStack.pop());
}catch(Exception e){
System.out.println(e.getMessage());
}
break;
case"exit":
scanner.close();
loop = false;
break;
default:
break;
}
}
System.out.println("over!");
}
}
4.4 栈实现计算器(两位数加减乘除)(中缀表达式)
4.4.1 图解分析
4.4.2 代码实现
public class ArrayStack2 {
private int maxSize;//栈的大小
private int[] stack;//数组模拟栈
private int top = -1;//top表示栈顶,初始化为-1
//构造器
public ArrayStack2(int maxSize) {
this.maxSize = maxSize;
stack = new int[this.maxSize];
}
//栈满
public boolean isFull(){
return top == maxSize - 1;
}
//栈空
public boolean isEmpty(){
return top==-1;
}
//入栈
public void push(int value){
if(isFull()){
System.out.println("栈已满!");
return;
}
stack[++top] = value;
}
//出栈
public int pop(){
if(isEmpty()){
throw new RuntimeException("栈空!");
}
return stack[top--];
}
//遍历栈(从栈顶开始显示数据)
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]);
}
}
//查看栈顶数据(不是出栈)
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 = num2 + num1;
break;
case'-':
res = num2 - num1;//num2为后出栈的数,也就是先入栈的数,为被减数
break;
case'*':
res = num2 * num1;
break;
case'/':
res = num2 / num1;//同减法
break;
}
return res;
}
}
public class Calculator {
public static void main(String[] args) {
String expression = "5+10*5-5*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
String keepNum = "";//用于拼接多位数
//扫描expression
while(true){
ch = expression.substring(index,index+1).charAt(0);
//判断index
if(operStack.isOper(ch)){//如果是运算符
if(!operStack.isEmpty()){
/*如果运算符栈不为空
* 如果当前运算符优先级小于或等于栈顶运算符,则
* ※数据栈中pop出两个数
* ※运算符栈中pop出一个符号
* 运算完毕将结果入数据站,当前的操作符入符号栈
* */
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);//ASCII码的1对应为49
/*
* 处理多位数时,需要向expression的表达式index后再看一位
* 如果是数就进行扫描,如果是符号就入栈
* 用keepNum拼接
* */
//处理多位数
keepNum += ch;
//如果ch已经是expression的最后一位,就直接入栈
if(index == expression.length()-1){
numStack.push(Integer.parseInt(keepNum));
}else{
//判断下一个字符是不是数字,如果是数字,就继续扫描,如果是运算符,则入栈
if(operStack.isOper(expression.substring(index+1,index+2).charAt(0))){
//如果后一位是运算符,则入栈keepNum=“1”或者“123”
numStack.push(Integer.parseInt(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.println("表达式"+expression+"结果是:"+numStack.pop());
}
}
4.5 逆波兰计算器(逆波兰表达式:后缀表达式)
4.5.1 思路分析
如: (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 - , 针对后缀表达式求值步骤如下:
- 从左至右扫描,将 3 和 4 压入堆栈;
- 遇到+运算符,因此弹出 4 和 3(4 为栈顶元素,3 为次顶元素),计算出 3+4 的值,得 7,再将 7 入栈;
- 将 5 入栈;
- 接下来是×运算符,因此弹出 5 和 7,计算出 7×5=35,将 35 入栈;
- 将 6 入栈;
- 最后是-运算符,计算出 35-6 的值,即 29,由此得出最终结果
4.5.2 代码实现
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
/*
* 逆波兰计算器
* 逆波兰表达式:后缀表达式
* */
public class PolandNotation {
public static void main(String[] args) {
/*
* 后缀表达式,也称为逆波兰表达式
* 中缀为:(3+4)*5-6
* 为了方便用空格隔开
* */
String suffixExpression = "3 4 + 50 * 6 -";
List<String> listString = getListString(suffixExpression);
System.out.println(listString);
System.out.println(calculate(listString));
}
//依次将逆波兰表达式中的数据和运算符放到ArrayList中
public static List<String> getListString(String suffixExpression){
String[] split = suffixExpression.split(" ");
List<String> list = new ArrayList<>();
for (String ele : split) {
list.add(ele);
}
return list;
}
//完成对逆波兰表达式的计算
public static int calculate(List<String> list){
//创建一个栈(只需要一个栈即可)
Stack<String> stack = new Stack<String>();
for (String item : list) {
if(item.matches("\\d+")) {//正则表达式,表示多位数
stack.push(item);//数字直接入栈
}else{
int num2 = Integer.parseInt(stack.pop());//栈顶
int num1 = Integer.parseInt(stack.pop());//次栈顶
int res = 0;
if(item.equals("+")){
res = num1 + num2;
}else if(item.equals("-")){
res = num1 - num2;
}else if(item.equals("*")){
res = num1 * num2;
}else if(item.equals("/")){
res = num1 / num2;
}else{
throw new RuntimeException("运算符有误");
}
//res入栈
stack.push(""+res );//把整数res转换为字符串压入栈
}
}
return Integer.parseInt(stack.pop());
}
}
4.6 中缀表达式转换为后缀表达式
4.6.1 步骤:
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 中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式
4.6.2 示例图解
“1+((2+3)×4)-5”——> "1 2 3 + 4 × + 5 –"
4.6.3 代码实现
在4.5.2代码的基础上添加以下方法和类:
//将中缀表达式转成对应的list
public static List<String> toInfixExpressionList(String s);
//将得到的【中缀表达式对应的List】转换为【后缀表达式对应的List】
public static List<String> parseSuffixExpressionList(List<String> list);
/*
* 运算符类,可以返回一个运算符对应的优先级
* */
public class Operation;
完整代码如下:
/*
* 运算符类,可以返回一个运算符对应的优先级
* */
public class Operation {
private static int ADD = 1;
private static int SUB = 1;
private static int MUL = 2;
private static int DIV = 2;
public static int getValue(String op){
int result = 0;
switch(op){
case"+":
result = ADD;
break;
case"-":
result = SUB;
break;
case"*":
result = MUL;
break;
case"/":
result = DIV;
break;
default:
System.out.println("不存在该运算符");
break;
}
return result;
}
}
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
public class PolandNotation {
public static void main(String[] args) {
String expression = "1+((2+3)*4)-5";
System.out.println("原式:"+expression);
List<String> toInfixExpressionList = toInfixExpressionList(expression);
System.out.println("中缀:"+toInfixExpressionList);
List<String> parseSuffixExpressionList = parseSuffixExpressionList(toInfixExpressionList);
System.out.println("后缀:"+parseSuffixExpressionList);
System.out.println(expression+"="+calculate(parseSuffixExpressionList));
}
//将中缀表达式转成对应的list
public static List<String> toInfixExpressionList(String s){
ArrayList<String> list = new ArrayList<>();
int i = 0;//指针,用于遍历中缀表达式字符串
String str;//对多位数进行拼接
char c;//每次遍历到的字符
do {
//如果c不是数字,则直接添加到list中
if((c=s.charAt(i))<48||(c=s.charAt(i))>57){
list.add(""+c);//c转换为字符串添加进list
i++;
}else{//如果c是一个数字,需要考虑多位数
str = "";
while(i<s.length()&&(c=s.charAt(i))>=48&&(c=s.charAt(i))<=57){
str += c;//拼接多位数
i++;
}
list.add(str);
}
}while(i<s.length());
return list;
}
//将得到的【中缀表达式对应的List】转换为【后缀表达式对应的List】
public static List<String> parseSuffixExpressionList(List<String> list) {
Stack<String> s1 = new Stack<>();//符号栈
/*
* Stack<String> s2 = new Stack<>();//中间结果栈
* 因为s2栈在转换过程中并没有pop操作,且后续需要逆序输出
* 所以用栈比较麻烦,改用List<String> s2
* */
List<String> s2 = new ArrayList<>();//存储中间结果
for (String item : list) {
if (item.matches("\\d+")) {//如果是一个数直接加入s2
s2.add(item);
} else if (item.equals("(")) {//左括号直接加入s1
s1.push(item);
} else if (item.equals(")")) {//右括号(右括号不入栈)
// 则依次弹出s1栈顶的运算符并压入s2,直到遇到左括号,此时丢弃这一对括号
while (!s1.peek().equals("(")) {
s2.add(s1.pop());
}
s1.pop();//!!!左括号 从s1栈中弹出,丢弃这一对小括号
} else {
//当item的优先级小于等于s1栈顶运算符,将s1栈顶的运算符弹出并加入到s2中
while (s1.size() != 0 && Operation.getValue(s1.peek()) >= Operation.getValue(item)) {
s2.add(s1.pop());
}
//将item压入栈
s1.push(item);
}
}
//将s1中剩余的运算符依次弹出并加入到s2
while (s1.size() != 0) {
s2.add(s1.pop());
}
return s2;//s2
}
//依次将逆波兰表达式中的数据和运算符放到ArrayList中
public static List<String> getListString(String suffixExpression){
String[] split = suffixExpression.split(" ");
List<String> list = new ArrayList<>();
for (String ele : split) {
list.add(ele);
}
return list;
}
//完成对逆波兰表达式的计算
public static int calculate(List<String> list){
//创建一个栈(只需要一个栈即可)
Stack<String> stack = new Stack<String>();
for (String item : list) {
if(item.matches("\\d+")) {//正则表达式,表示多位数
stack.push(item);//数字直接入栈
}else{
int num2 = Integer.parseInt(stack.pop());//栈顶
int num1 = Integer.parseInt(stack.pop());//次栈顶
int res = 0;
if(item.equals("+")){
res = num1 + num2;
}else if(item.equals("-")){
res = num1 - num2;
}else if(item.equals("*")){
res = num1 * num2;
}else if(item.equals("/")){
res = num1 / num2;
}else{
throw new RuntimeException("运算符有误");
}
//res入栈
stack.push(""+res );//把整数res转换为字符串压入栈
}
}
return Integer.parseInt(stack.pop());
}
}
4.7 逆波兰计算器完整版*
4.7.1 功能
- 支持 + - * / ( )
- 多位数,支持小数,
- 兼容处理, 过滤任何空白字符,包括空格、制表符、换页符
4.7.2 代码实现*
package tx.stack;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Stack;
import java.util.regex.Pattern;
public class ReversePolishMultiCalc {
/**
* 匹配 + - * / ( ) 运算符
*/
static final String SYMBOL = "\\+|-|\\*|/|\\(|\\)";
static final String LEFT = "(";
static final String RIGHT = ")";
static final String ADD = "+";
static final String MINUS= "-";
static final String TIMES = "*";
static final String DIVISION = "/";
/**
* 加減 + -
*/
static final int LEVEL_01 = 1;
/**
* 乘除 * /
*/
static final int LEVEL_02 = 2;
/**
* 括号
*/
static final int LEVEL_HIGH = Integer.MAX_VALUE;
static Stack<String> stack = new Stack<>();
static List<String> data = Collections.synchronizedList(new ArrayList<String>());
/**
* 去除所有空白符
* @param s
* @return
*/
public static String replaceAllBlank(String s ){
// \\s+ 匹配任何空白字符,包括空格、制表符、换页符等等, 等价于[ \f\n\r\t\v]
return s.replaceAll("\\s+","");
}
/**
* 判断是不是数字 int double long float
* @param s
* @return
*/
public static boolean isNumber(String s){
Pattern pattern = Pattern.compile("^[-\\+]?[.\\d]*$");
return pattern.matcher(s).matches();
}
/**
* 判断是不是运算符
* @param s
* @return
*/
public static boolean isSymbol(String s){
return s.matches(SYMBOL);
}
/**
* 匹配运算等级
* @param s
* @return
*/
public static int calcLevel(String s){
if("+".equals(s) || "-".equals(s)){
return LEVEL_01;
} else if("*".equals(s) || "/".equals(s)){
return LEVEL_02;
}
return LEVEL_HIGH;
}
/**
* 匹配
* @param s
* @throws Exception
*/
public static List<String> doMatch (String s) throws Exception{
if(s == null || "".equals(s.trim())) throw new RuntimeException("data is empty");
if(!isNumber(s.charAt(0)+"")) throw new RuntimeException("data illeagle,start not with a number");
s = replaceAllBlank(s);
String each;
int start = 0;
for (int i = 0; i < s.length(); i++) {
if(isSymbol(s.charAt(i)+"")){
each = s.charAt(i)+"";
//栈为空,(操作符,或者 操作符优先级大于栈顶优先级 && 操作符优先级不是( )的优先级 及是 ) 不能直接入栈
if(stack.isEmpty() || LEFT.equals(each)
|| ((calcLevel(each) > calcLevel(stack.peek())) && calcLevel(each) < LEVEL_HIGH)){
stack.push(each);
}else if( !stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek())){
//栈非空,操作符优先级小于等于栈顶优先级时出栈入列,直到栈为空,或者遇到了(,最后操作符入栈
while (!stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek()) ){
if(calcLevel(stack.peek()) == LEVEL_HIGH){
break;
}
data.add(stack.pop());
}
stack.push(each);
}else if(RIGHT.equals(each)){
// ) 操作符,依次出栈入列直到空栈或者遇到了第一个)操作符,此时)出栈
while (!stack.isEmpty() && LEVEL_HIGH >= calcLevel(stack.peek())){
if(LEVEL_HIGH == calcLevel(stack.peek())){
stack.pop();
break;
}
data.add(stack.pop());
}
}
start = i ; //前一个运算符的位置
}else if( i == s.length()-1 || isSymbol(s.charAt(i+1)+"") ){
each = start == 0 ? s.substring(start,i+1) : s.substring(start+1,i+1);
if(isNumber(each)) {
data.add(each);
continue;
}
throw new RuntimeException("data not match number");
}
}
//如果栈里还有元素,此时元素需要依次出栈入列,可以想象栈里剩下栈顶为/,栈底为+,应该依次出栈入列,可以直接翻转整个 stack 添加到队列
Collections.reverse(stack);
data.addAll(new ArrayList<>(stack));
System.out.println(data);
return data;
}
/**
* 算出结果
* @param list
* @return
*/
public static Double doCalc(List<String> list){
Double d = 0d;
if(list == null || list.isEmpty()){
return null;
}
if (list.size() == 1){
System.out.println(list);
d = Double.valueOf(list.get(0));
return d;
}
ArrayList<String> list1 = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
list1.add(list.get(i));
if(isSymbol(list.get(i))){
Double d1 = doTheMath(list.get(i - 2), list.get(i - 1), list.get(i));
list1.remove(i);
list1.remove(i-1);
list1.set(i-2,d1+"");
list1.addAll(list.subList(i+1,list.size()));
break;
}
}
doCalc(list1);
return d;
}
/**
* 运算
* @param s1
* @param s2
* @param symbol
* @return
*/
public static Double doTheMath(String s1,String s2,String symbol){
Double result ;
switch (symbol){
case ADD : result = Double.valueOf(s1) + Double.valueOf(s2); break;
case MINUS : result = Double.valueOf(s1) - Double.valueOf(s2); break;
case TIMES : result = Double.valueOf(s1) * Double.valueOf(s2); break;
case DIVISION : result = Double.valueOf(s1) / Double.valueOf(s2); break;
default : result = null;
}
return result;
}
public static void main(String[] args) {
//String math = "9+(3-1)*3+10/2";
String math = "12.8 + (2 - 3.55)*4+10/5.0";
try {
doCalc(doMatch(math));
} catch (Exception e) {
e.printStackTrace();
}
}
}
五、递归
5.1 递归概念和调用机制
5.1.1 概念
递归就是方法自己调用自己,每次调用时传入不同的变量。递归有助于编程者解决复杂的问题,同时可以让代码变得简洁。
5.1.2 调用机制(阶乘问题代码演示)
public class RecursionTest {
public static void main(String[] args) {
System.out.println(factorial(5));
}
public static int factorial(int n){
if(n==1){
return 1;
}else{
return factorial(n-1)*n;
}
}
}
5.2 递归能解决的问题
- 各种数学问题如: 8 皇后问题 ,汉诺塔, 阶乘问题,迷宫问题, 球和篮子的问题
- 各种算法中也会使用到递归,比如快排,归并排序,二分查找,分治算法等
- 将用栈解决的问题——>第归代码比较简洁
5.3 使用递归需遵守的规则
- 执行一个方法时,就创建一个新的受保护的独立空间(栈空间)
- 方法的局部变量是独立的,不会相互影响, 比如 n 变量
- 如果方法中使用的是引用类型变量(比如数组),就会共享该引用类型的数据.
- 递归 必须向退出递归的条件逼近,否则就是无限递归,出现 StackOverflowError,死龟了:)
- 当一个方法执行完毕,或者遇到 return,就会返回, 遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕
5.4 迷宫问题
5.4.1 图示
5.4.2 代码实现
/*
* 迷宫问题
* */
public class Maze {
public static void main(String[] args) {
//创建二维数组,模拟迷宫地图
int[][] map = new int[8][7];
//上下左右全部置为 1 ,表示墙
for (int i = 0; i < 7; i++) {//列变行不变
map[0][i] = 1;
map[7][i] = 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;
System.out.println("迷宫地图:");
for (int[] ints : map) {
for (int anInt : ints) {
System.out.print(anInt+" ");
}
System.out.println();
}
setWay(map,1,1);
System.out.println("小球走过并标记过的路线:");
for (int[] ints : map) {
for (int anInt : ints) {
System.out.print(anInt+" ");
}
System.out.println();
}
}
//使用递归找路
/**
* @param map 地图
* @param i 初始位置横坐标
* @param j 初始位置纵坐标
* @return 找到路返回true
*
* 约定:1.map[6][5]为终点,如果小球能走到,则通路找到
* 2.map[i][j]为0表示没有走过当前位置,为1表示为墙,为2表示通路可以走,为3表示该点已经走过(判断过),走不通
* 3.确定一个策略(方法) 下->右->上->左 (最终得到的路径和设置的策略有关系)
*/
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]不为0,可能为:1,2,3
return false;
}
}
}
}
运行结果:
5.5 八皇后问题
八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848 年提出:在 8×8 格的国际象棋上摆放八个皇后,使其不能互相攻击,即: 任意两个皇后都不能处于同一行 、同一列或同一斜线上,问有多少种摆法(92)。
5.5.1 思路分析
- 第一个皇后先放第一行第一列
- 第二个皇后放在第二行第一列、然后判断是否跟别的皇后冲突(不能在同一行 、同一列或同一斜线上), 如果冲突,继续放在第二列、第三列、依次把所有列都放完,找到一个合适
- 继续第三个皇后,还是第一列、第二列……直到第 8 个皇后也能放在一个不冲突的位置,算是找到了一个正确解
- 当得到一个正确解时,在栈回退到上一个栈时,就会开始回溯,即将第一个皇后,放到第一列的所有正确解,全部得到.
- 然后回头继续第一个皇后放第二列,后面继续循环执行 1,2,3,4 的步骤
- 示意图:
注意:理论上应该创建一个二维数组来表示棋盘,但是实际上可以通过算法,用一个一维数组即可解决问题。例如:arr[8] ={0 , 4, 7, 5, 2, 6, 1, 3} //对应 arr 下标 表示第几行,即第几个皇后(第几行就代表第几个皇后,因为一共有几行就有几个皇后!);arr[i] = val , val 表示第 i+1 个皇后,放在第 i+1行的第 val+1 列。
※:arr[8] ={0 , 4, 7, 5, 2, 6, 1, 3} 第1个皇后放在第1行的第1列,第2个皇后放在第2行的第5列,依次类推。
5.5.2 代码实现
/*
* 八皇后问题
* */
public class Queen8 {
int max = 8;//8个皇后
static int count = 0;
static int judgeCount = 0;
int[] array = new int[max];//保存皇后放置的位置。例如:arr[8] ={0 , 4, 7, 5, 2, 6, 1, 3}
public static void main(String[] args) {
Queen8 queen8 = new Queen8();
queen8.check(0);//先放第一个皇后
System.out.println("共有"+count+"种解法");//92种
System.out.println("共判断"+judgeCount+"次");//15720次
}
//放置第n个皇后
//【注意:每一次递归时,进入check中的都有for循环,所有会有回溯
private void check(int n){
if(n==max){//n为8时,已经放置好了8个皇后
print();
return;
}
//依次放入皇后,并判断是否冲突
for (int i = 0; i < max; i++) {
array[n] = i;//先把当前这个皇后放到第1列
if(judge(n)){//true为不冲突
check(n+1);//递归调用,接着放第n+1个皇后
}//如果冲突了,则继续执行下一次循环(i++)
}
}
//查看放置的第n个皇后,检测是否和前面已经摆放皇后是否冲突
/**
* @param n 表示第n个皇后
* @return
*/
private boolean judge(int n){//数组索引代表第几行(索引从0开始,0代表第一行
judgeCount++;
for (int i = 0; i < n; i++) {
/**
* array[i]==array[n] // 判断是否在同一列
* Math.abs(n-i)==Math.abs(array[n] - array[i]) // |行差|==|列差|时就在同一条斜线上
* 【同一行没必要判断,因为n每次都在递增】
*/
if(array[i]==array[n] || Math.abs(n-i)==Math.abs(array[n] - array[i]) ){
return false;
}
}
return true;
}
//打印皇后摆放位置
private void print(){
count++;
for (int i = 0; i < array.length; i++) {
System.out.print(array[i]+" ");
}
System.out.println();
}
}
六、排序算法
排序也称排序算法(SortAlgorithm),排序是将一组数据,按照指定的顺序进行排列的过程。
6.1 分类
- 内部排序: 指将需要处理的所有数据都加载到 内部存储器( 内存)中进行排序。
- 外部排序:数据量过大,无法全部加载到内存中,需要借助 外部存储( 文件等)进行排序。
6.2 算法的时间复杂度
1. 事后统计的方法
这种方法可行, 但是有两个问题:一是要想对设计的算法的运行性能进行评测,需要实际运行 该程序;二是所得时间的统计量依赖于计算机的硬件、软件等环境因素, 这种方式,要在同一台计算的相同状态下运行,才能比较那个算法速度更快。
2. 事前估算的方法
通过分析某个算法的时间复杂度来判断哪个算法更优。
6.2.1 时间频度
时间频度:一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。 一个算法中的语句执行次数称为语句频度或时间频度。记为 T(n)。
①:计算1-100的和,设计两种方法:
②:忽略常数项
结论:
1. 2n+20 和 2n 随着 n 变大,执行曲线无限接近, 20 可以忽略
2. 3n+10 和 3n 随着 n 变大,执行曲线无限接近, 10 可以忽略
③:忽略低次项
结论:
1. 2n^2+3n+10 和 2n^2 随着 n 变大, 执行曲线无限接近, 可以忽略 3n+10
2. n^2+5n+20 和 n^2 随着 n 变大,执行曲线无限接近, 可以忽略 5n+20
④:忽略系数
结论:
1. 随着 n 值变大,5n^2+7n 和 3n^2 + 2n ,执行曲线重合, 说明这种情况下, 5和3可以忽略(理解:求极限可近似为1)。
2. 而 n^3+5n 和 6n^3+4n ,执行曲线分离,说明指数关键 ?(高次方的系数不能随便忽略!)(理解:求极限为6,同阶无穷小)
6.2.2 时间复杂度
1. 一般情况下, 算法中的基本操作语句的重复执行次数是问题规模 n 的某个函数,用 T(n)表示,若有某个辅助函数 f(n),使得当 n 趋近于无穷大时,T(n) / f(n) 的极限值为不等于零的常数,则称 f(n)是 T(n)的同数量级函数。记作 T(n)= O( f(n) ),称O( f(n) ) 为算法的渐进时间复杂度,简称时间复杂度。
2. T(n) 不同,但时间复杂度可能相同。 如:T(n)=n²+7n+6 与 T(n)=3n²+2n+2 它们的 T(n) 不同,但时间复杂度相同,都为 O(n²)。
3. 计算时间复杂度的方法
- 用常数 1 代替运行时间中的所有加法常数。例如:T(n)=n²+7n+6 => T(n)=n²+7n+1
- 修改后的运行次数函数中,只保留最高阶项 。例如:T(n)=n²+7n+1 => T(n) = n²
- 去除最高阶项的系数 。例如:T(n) = n² => T(n) = n² => O(n²)
6.2.3 常见的时间复杂度
常见的算法时间复杂度由小到大依次为:Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n2)<Ο(n3)< Ο(nk) <Ο(2n) ,随着问题规模 n 的不断增大,上述时间复杂度不断增大,算法的执行效率越低。从下图可知,应该尽可能避免使用指数阶的算法。
1. 常数阶 O(1)
2. 对数阶 O(log2n)
3. 线性阶 O(n)
4. 线性对数阶 O(nlog2n)
5. 平方阶 O(n^2)
6. 立方阶 O(n^3)
7. k 次方阶 O(n^k)
8. 指数阶 O(2^n)
6.2.4 平均时间复杂度和最坏时间复杂度
- 平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下,该算法的运行时间。
- 最坏情况下的时间复杂度称最坏时间复杂度。 一般讨论的时间复杂度均是最坏情况下的时间复杂度。这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的界限,这就保证了算法的运行时间不会比最坏情况更长。
- 平均时间复杂度和最坏时间复杂度是否一致,和算法有关
6.3 空间复杂度
- 类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是问题规模 n 的函数。
- 空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。有的算法需要占用的临时工作单元数与解决问题的规模 n 有关,它随着 n 的增大而增大,当 n 较大时,将占用较多的存储单元,例如快速排序和 归并排序算法, 基数排序就属于这种情况
- 在做算法分析时,主要讨论的是时间复杂度。 从用户使用体验上看 , 更看重的程序执行的速度。一些缓存产品(redis, memcache)和算法(基数排序)本质就是用空间换时间
6.4 冒泡排序
冒泡排序(Bubble Sorting)的基本思想是:通过对待排序序列从前向后(从下标较小的元素开始), 依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。
优化:
因为排序的过程中,各元素不断接近自己的位置, 如果一趟比较下来没有进行过交换 , 就说明序列有序,因此要在排序过程中设置一个标志 flag 判断元素是否进行过交换,从而减少不必要的比较。
6.4.1 图解
(1) 一共进行 数组的大小-1 次 大的循环
(2)每一趟排序的次数在逐渐的减少
(3) 如果发现在某趟排序中,没有发生一次交换, 可以提前结束冒泡排序。
6.4.2 代码实现
public class BubbleSort {
public static void main(String[] args) {
int[] arr = {3, 9, -1, 10, 20};
System.out.println("冒泡前:" + Arrays.toString(arr));
bubbleSort(arr);
System.out.println("冒泡后:" + Arrays.toString(arr));
}
public static void bubbleSort(int[] arr){
int temp = 0;
boolean flag = false;//标识是否进行过交换
//冒泡排序的时间复杂度:O(n²)
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]){
flag = true;
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
if(!flag){//说明在一趟排序中,没有交换过任何一个数据的位置
break;
}else{
flag = false;//重置flag,进行下一次的判断
}
System.out.println("第"+(i+1)+"趟排序后的数组:"+Arrays.toString(arr));
}
}
}
6.5 选择排序
选择式排序也属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的。
基本思想:第一次从 arr[0]~arr[n-1]中选取最小值,与 arr[0]交换,第二次从 arr[1]~arr[n-1]中选取最小值,与 arr[1]交换,第三次从 arr[2]~arr[n-1]中取最小值,与 arr[2]交换,…,第 i 次从 arr[i-1]~arr[n-1]中选取最小值,与 arr[i-1]交换,…, 第 n-1 次从 arr[n-2]~arr[n-1]中选取最小值,与 arr[n-2]交换,总共通过 n-1 次,得到一个按排序码从小到大排列的有序序列。
6.5.1 图解
6.5.2 代码实现
import java.util.Arrays;
public class SelectSort {
public static void main(String[] args) {
int[] arr = {101,34,119,1,-3,0};
System.out.println("选择前:"+Arrays.toString(arr));
System.out.println("选择后:");
selectSort(arr);
}
public static void selectSort(int[] arr){
//选择排序时间复杂度O(n²)
for (int i = 0; i < arr.length-1; i++) {
int minIndex = i;
int min = arr[minIndex];
for (int j = i+1; j < arr.length; j++) {
if(min>arr[j]){
min = arr[j];
minIndex = j;
}
}
if(minIndex!=i){//将此轮比较所得最小值放在i位置
arr[minIndex] = arr[i];
arr[i] = min;
}
System.out.println("第"+(i+1)+"趟排序后的数组:"+Arrays.toString(arr));
}
System.out.println(Arrays.toString(arr));
}
}
6.6 插入排序
插入式排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的。
基本思想:把 n 个待排序的元素看成为一个有序表和一个无序表,开始时 有序表中只包含1个元素,无序表中包含有 n-1 个元素,排序过程中每次从无序表中取出第1个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。
6.6.1 图解
6.6.2 代码实现
import java.util.Arrays;
public class InsertSort {
public static void main(String[] args) {
int[] arr = {101,34,119,1,-4,0};
System.out.println("插入前:"+ Arrays.toString(arr));
System.out.println("插入后:");
insertSort(arr);
}
public static void insertSort(int[] arr){
for (int i = 1; i < arr.length; i++) {//从第二个数字开始,到最后一个数, arr.length不需要减1
int insertVal = arr[i];
int insertIndex = i-1;
while(insertIndex>=0 && insertVal < arr[insertIndex]){
arr[insertIndex+1] = arr[insertIndex];
insertIndex--;
}
//判断是否需要赋值
if (insertIndex + 1 != i) {
arr[insertIndex+1] = insertVal;
}
System.out.println("第"+i+"轮插入排序后:"+Arrays.toString(arr));
}
System.out.println(Arrays.toString(arr));
}
}
6.7 希尔排序
希尔排序也是一种 插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。
基本思想:希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多, 当增量减至1时,整个文件恰被分成一组,算法便终止。
6.7.1 图解
6.7.2 代码实现
(交换法和移动法,移动法优)
import java.util.Arrays;
public class ShellSort {
public static void main(String[] args) {
int[] arr = {8,9,1,7,2,3,5,4,6,0};
System.out.println("希尔前:"+ Arrays.toString(arr));
System.out.println("希尔交换排序后:");
shellSort(arr);//交换式
System.out.println("希尔移位排序后:");
shellSort2(arr);//移位式
}
//交换式的希尔排序
public static void shellSort(int[] arr){
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组,每组arr.length/gap个元素),步长为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));
}
/*
【分步推导!】
//1.将10个数据分成10/2=5组
for (int i = 5; i < arr.length; i++) {
//遍历各组中所有的元素(共5组,每组2个元素),步长为5
for(int j = i - 5; j >=0; j -= 5){
if(arr[j] > arr[j+5]){//如果当前元素大于加上步长后的元素,交换
temp = arr[j];
arr[j] = arr[j+5];
arr[j+5] = temp;
}
}
}
System.out.println("第1轮希尔排序后:"+Arrays.toString(arr));
//2.将10个数据分成5/2=2组
for (int i = 2; i < arr.length; i++) {
//遍历各组中所有的元素(共2组,每组5个元素),步长为2
for(int j = i - 2; j >=0; j -= 2){
if(arr[j] > arr[j+2]){//如果当前元素大于加上步长后的元素,交换
temp = arr[j];
arr[j] = arr[j+2];
arr[j+2] = temp;
}
}
}
System.out.println("第2轮希尔排序后:"+Arrays.toString(arr));
//3.将10个数据分成2/2=1组
for (int i = 1; i < arr.length; i++) {
//遍历各组中所有的元素(共1组,每组10个元素),步长为1
for(int j = i - 1; j >=0; j -= 1){
if(arr[j] > arr[j+1]){//如果当前元素大于加上步长后的元素,交换
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
System.out.println("第3轮希尔排序后:"+Arrays.toString(arr));*/
}
//改进交换式的希尔排序-->移位式
public static void shellSort2(int[] arr){
//增量gap,逐步缩小增量
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]){//下标还没到0,没找到插入位置,可以继续往前找
arr[j] = arr[j-gap];//arr[j-gap]大,移动到j这个位置
j -= gap;//前移
}
//退出while之后就找到temp插入的位置
arr[j] = temp;
}
}
}
System.out.println(Arrays.toString(arr));
}
}
6.8 快速排序
快速排序(Quicksort)是对冒泡排序的一种改进。
基本思想:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序, 整个排序过程可以递归进行,以此达到整个数据变成有序序列。
6.8.1 图解
6.8.2 代码实现(不太明白……)
import java.util.Arrays;
public class QuickSort {
public static void main(String[] args) {
int[] arr = {101,34,-5,71,-9,78,0,23,70,-567};
System.out.println("快速排序前:"+Arrays.toString(arr));
quickSort(arr,0,arr.length-1);
System.out.println("快速排序后:"+Arrays.toString(arr));
}
public static void quickSort(int[] arr,int left,int right){
int l = left;
int r = right;
int pivot = arr[(left+right)/2];//中轴值
int temp = 0;//临时变量
while(l<r){//小于pivot的值放左边,大于pivot的值放右边
while(arr[l]<pivot){//在pivot左边一直找,直到找到大于pivot的值才退出
l += 1;
}
while(arr[r]>pivot){//在pivot右边一直找,直到找到小于pivot的值才退出
r -= 1;
}
if(l>=r){//如果l>=r,说明pivot左边都是小于等于pivot的,右边都是大于等于pivot的
break;
}
//交换
temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
/*
* 左边索引超过中间值的索引,只让R左移,右边索引超过中间值的索引,只让L右移。
* 这样就能保证一直以中间值为轴,进行左右互换。防止左右和pivot值一样时陷入死循环!
*
* 下面两个if自己理解:【pivot是一个值!和位置无关】
*arr[l]==pivot:
* 比pivot值小(应该放在pivot左边)的值已经和pivot值一样大了!
* 没有比pivot值更小的值了,
*arr[r]==pivot:
* 比pivot值大(应该放在pivot右边)的值已经和pivot值一样大了!
* 没有比pivot值更大的值了,
* */
//如果交换完后,发现这个arr[l]==pivot值,r--,前移
if(arr[l]==pivot){
r -= 1;
}
//如果交换完后,发现这个arr[r]==pivot值,l++,后移
if(arr[r]==pivot){
l += 1;
}
//如果l == r,必须l++,r--,否则会栈溢出
if(l == r){
l += 1;
r -= 1;
}
//向左递归
if(left < r){
quickSort(arr,left,r);
}
//向右递归
if(right > l){
quickSort(arr,l,right);
}
}
}
}
6.8.3 能看明白的快去排序(以第一个数为基准数)
6.9 归并排序
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治 (divide-and-conquer 策略分治法将问题分(divide)成一些 小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
6.9.1 图解
6.9.2 代码实现
import java.util.Arrays;
public class MergeSort {
public static void main(String[] args) {
int[] arr = {8, 4, 5, 7, 1, 3, 6, 2};
System.out.println("归并排序前:" + Arrays.toString(arr));
int[] temp = new int[arr.length];//归并排序需要一个额外的空间开销
mergeSort(arr, 0, arr.length - 1, temp);
System.out.println("归并排序后:" + Arrays.toString(arr));
}
//分+合方法
public static void mergeSort(int[] arr, int left, int right, int[] temp) {
if (left < right) {
int mid = (left + right) / 2;
mergeSort(arr, left, mid, temp);//向左递归进行分解
mergeSort(arr, mid + 1, right, temp);//向右递归进行分解
merge(arr, left, mid, right, temp);//合并
}
}
/**
* @param arr 原始数组
* @param left 左边有序序列的初始索引
* @param mid 中间索引(左边序列的结束,mid+1就是右边序列的初始化索引)
* @param right 右边索引
* @param temp 做中转的数组
*/
public static void merge(int[] arr, int left, int mid, int right, int[] temp) {
System.out.println("调用");
int i = left;//初始化i,左边有序 序列的初始化索引
int j = mid + 1;//初始化j, 右边有序序列的初始化索引
int t = 0;//指向temp数组的当前索引
//1. 把左右两边有序数组的数据按照规则填充到temp中,直到有一边序列的处理完毕
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {//左边有序序列的当前元素<=右边有序序列的当前元素
temp[t] = arr[i];//将左边的当前元素填充到temp数组
t += 1;
i += 1;
} else {//将右边有序序列的当前元素填充到temp数组
temp[t] = arr[j];
t += 1;
j += 1;
}
}
//2. 把有剩余数据一边的所有数据依次全部填充到temp
while (i <= mid) {//将左边序列的剩余元素填充到temp
temp[t] = arr[i];
t += 1;
i += 1;
}
while (j <= right) {//将右边序列的剩余元素填充到temp
temp[t] = arr[j];
t += 1;
j += 1;
}
//3. 将temp数组复制到arr数组【!并不是每次都要复制8个数据】
t = 0;
int tempLeft = left;
System.out.println("tempLeft="+tempLeft+",right="+right);
while (tempLeft <= right) {
/*
* ① tempLeft=0, right=1
* ② tempLeft=2, right=3
* ③ tempLeft=0, right=3
* ……
* 最后一次:tempLeft=0, right=7
* */
arr[tempLeft] = temp[t];
t += 1;
tempLeft += 1;
}
}
}
6.10 基数排序
基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或 bin sort,顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用。基数排序法是效率高的稳定性排序法。基数排序(Radix Sort)是桶排序的扩展。
基本思想:将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
6.10.1 图解
6.10.2 代码实现
import java.util.Arrays;
public class RadixSort {
public static void main(String[] args) {
int[] arr = {53, 3, 1234, 542, 748, 14, 214};
radixSort(arr);
}
public static void radixSort(int[] arr) {
//需要得到数组中最大数的位数
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
int maxLength = (max + "").length();
//二维数组表示10个桶,每个桶就是一个一维数组。arr.length:防止极端情况下数组溢出
int[][] bucket = new int[10][arr.length];
//一维数组记录各个桶每次放入的数据个数。如: bucketElementCounts[0],记录的就是bucket[0]桶放入数据的个数
int[] bucketElementCounts = new int[10];
for (int i = 0, n = 1; i < maxLength; i++, n *= 10) {
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个桶(即第k个一维数组),放入
for (int l = 0; l < bucketElementCounts[k]; l++) {
arr[index++] = bucket[k][l];
}
}
//需要将每一个bucketElementCounts[k]置为0!!!
bucketElementCounts[k] = 0;
}
System.out.println("第" + (i + 1) + "轮基数排序对个位处理后:" + Arrays.toString(arr));
}
/*//1.针对每个元素的个位进行排序处理
for (int j = 0; j < arr.length; j++) {
int digitOfElement = arr[j] / 1 % 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个桶(即第k个一维数组),放入
for (int l = 0; l < bucketElementCounts[k]; l++) {
arr[index++] = bucket[k][l];
}
}
//需要将每一个bucketElementCounts[k]置为0!!!
bucketElementCounts[k] = 0;
}
System.out.println("第1轮基数排序对个位处理后:" + Arrays.toString(arr));
//2.针对每个元素的十位进行排序处理
for (int j = 0; j < arr.length; j++) {
int digitOfElement = arr[j] / 10 % 10;//取十位
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];//放到对应的桶中
bucketElementCounts[digitOfElement]++;
}
index = 0;
//遍历每一个桶,并将桶中的数据放入到原数组
for (int k = 0; k < bucketElementCounts.length; k++) {
if (bucketElementCounts[k] != 0) {//如果桶中有数据
//循环第k个桶(即第k个一维数组),放入
for (int l = 0; l < bucketElementCounts[k]; l++) {
arr[index++] = bucket[k][l];
}
}
//需要将每一个bucketElementCounts[k]置为0!!!
bucketElementCounts[k] = 0;
}
System.out.println("第2轮基数排序对个位处理后:" + Arrays.toString(arr));
//3.针对每个元素的百位进行排序处理
for (int j = 0; j < arr.length; j++) {
int digitOfElement = arr[j] / 100 % 10;//取百位(三位数不需要模10,超过三位数需要)
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];//放到对应的桶中
bucketElementCounts[digitOfElement]++;
}
index = 0;
//遍历每一个桶,并将桶中的数据放入到原数组
for (int k = 0; k < bucketElementCounts.length; k++) {
if (bucketElementCounts[k] != 0) {//如果桶中有数据
//循环第k个桶(即第k个一维数组),放入
for (int l = 0; l < bucketElementCounts[k]; l++) {
arr[index++] = bucket[k][l];
}
}
//需要将每一个bucketElementCounts[k]置为0!!!
bucketElementCounts[k] = 0;
}
System.out.println("第3轮基数排序对个位处理后:" + Arrays.toString(arr));*/
}
}
6.10.3 一些说明
- 基数排序是对传统桶排序的扩展,速度很快。
- 基数排序是经典的空间换时间的方式,占用内存很大, 当对海量数据排序时,容易造成 OutOfMemoryError 。
- 基数排序时稳定的。[注:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且 r[i]在 r[j]之前,而在排序后的序列中,r[i]仍在 r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的]。
- 对于有负数的数组,可以采取求绝对值后排序再逆序的方法进行排序。
6.11 常用排序算法总结和对比
6.11.1 图示
6.11.2 相关术语解释
- 稳定:如果 a 原本在 b 前面,而 a=b,排序之后 a 仍然在 b 的前面;
- 不稳定:如果 a 原本在 b 的前面,而 a=b,排序之后 a 可能会出现在 b 的后面;
- 内排序:所有排序操作都在内存中完成;
- 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
- 时间复杂度: 一个算法执行所耗费的时间。
- 空间复杂度:运行完一个程序所需内存的大小。
- n: 数据规模
- k: “桶”的个数
- In-place: 不占用额外内存
- Out-place: 占用额外内存
七、查找算法
7.1 常用查找算法
- 顺序(线性)查找
- 二分查找/折半查找
- 插值查找
- 斐波那契查找
7.2 线性查找算法
7.2.1 算法思想
顺序查找,遍历。
7.2.2 代码实现
public class SepSearch {
public static void main(String[] args) {
int[] arr = {1, 3, 65, 6, -3, 0};
System.out.println(seqSearch(arr, 65));
}
public static int seqSearch(int[] arr, int value) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == value) {
return i;
}
}
return -1;
}
}
7.3 二分查找算法
7.3.1 算法思想
7.3.2 代码实现
import java.util.ArrayList;
public class BinarySearch {
//二分查找数组必须有序
public static void main(String[] args) {
int[] arr = {-3,0,12, 12, 65, 65,65};
System.out.println(binarySearch(arr, 0, arr.length - 1, 0));
System.out.println(binarySearch2(arr, 0, arr.length - 1, 12));
}
/**
* @param arr
* @param left 左索引
* @param right 右索引
* @param value
* @return 找到返回下标,否则返回-1
*/
public static int binarySearch(int[] arr, int left, int right, int value) {
if (left > right) {
return -1;
}
int mid = (left + right) / 2;
if (value == arr[mid]) {
return mid;
} else if (value < arr[mid]) {//向左递归
return binarySearch(arr, left, mid - 1, value);
} else {//向右递归
return binarySearch(arr, mid + 1, right, value);
}
}
//如果要找的数重复出现,返回左右的下标索引
/*
*1.找到mid索引值,不要马上返回
*2. 向mid索引值的 左边 扫描,将所有满足value的元素的下标加入到集合中
*3. 向mid索引值的 右边 扫描,将所有满足value的元素的下标加入到集合中
* 4. 返回集合
* */
public static ArrayList binarySearch2(int[] arr, int left, int right, int value) {
if (left > right) {
return new ArrayList<Integer>();
}
int mid = (left + right) / 2;
int midVal = arr[mid];
if (value < midVal) {//向左递归
return binarySearch2(arr, left, mid - 1, value);
} else if (value > midVal) {//向右递归
return binarySearch2(arr, mid + 1, right, value);
} else {
ArrayList<Integer> resIndexList = new ArrayList<>();
//2. 向mid索引值的 左边 扫描,将所有满足value的元素的下标加入到集合中
int temp = mid - 1;
while (true) {
if (temp < 0 || arr[temp] != value) {//退出
break;
}
//否则,temp放入resIndexList
resIndexList.add(temp);
temp -= 1;//temp左移
}
resIndexList.add(mid);
//3. 向mid索引值的 右边 扫描,将所有满足value的元素的下标加入到集合中
temp = mid + 1;
while (true) {
if (temp > arr.length - 1 || arr[temp] != value) {//退出
break;
}
//否则,temp放入resIndexList
resIndexList.add(temp);
temp += 1;//temp右移
}
return resIndexList;
}
}
}
7.4 插值查找算法
7.4.1 算法思想
1. 插值查找原理介绍:
插值查找算法类似于二分查找,不同的是插值查找每次从自适应 mid 处开始查找。
2. 将折半查找中的求 mid 索引的公式 , low 表示左边索引 left, high 表示右边索引 right。
key 就是前面我们讲的 value。
3. int mid = low + (high - low) * (key - arr[low]) / (arr[high] - arr[low]) ;
对应前面的公式:
int mid = left + (right – left) * (value – arr[left]) / (arr[right] – arr[left]);
4. 举例
7.4.2 代码实现
import java.util.Arrays;
public class InsertValueSearch {
public static void main(String[] args) {
int[] arr = new int[100];
for (int i = 0; i < arr.length; i++) {
arr[i] = i + 1;
}
System.out.println(Arrays.toString(arr));
System.out.println(insertValueSearch(arr,0,arr.length-1,120));
}
//arr:有序数组
public static int insertValueSearch(int[] arr, int left, int right, int value) {
if (left > right || value < arr[0] || value > arr[arr.length - 1]) {//value小于最小的或者大于最大的
return -1;
}
int mid = left + (right - left) * (value - arr[left]) / (arr[right] - arr[left]);
int midValue = arr[mid];
if(value>midValue){//向右递归
return insertValueSearch(arr,mid+1,right,value);
}else if(value<midValue){//向左递归
return insertValueSearch(arr,left,mid-1,value);
}else{
return mid;
}
}
}
7.4.3 注意
- 对于数据量较大,关键字分布比较均匀的查找表来说,采用插值查找, 速度较快。
- 关键字分布不均匀的情况下,该方法不一定比折半查找要好
7.5 斐波那契(黄金分割法)查找算法
7.5.1 算法思想
黄金分割点是指把一条线段分割为两部分,使其中一部分与全长之比等于另一部分与这部分之比。取其前三位数字的近似值是 0.618。斐波那契数列 {1, 1, 2, 3, 5, 8, 13, 21, 34, 55 ,……} 发现斐波那契数列的两个相邻数的比例,无限接近 黄金分割值0.618。
7.5.2 图解
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 即可。顺序表长度增加后,新增的位置(从 n+1 到 F[k]-1 位置),都赋为 n 位置的值即可。
while(n>fib(k)-1)
k++;
7.5.3 代码实现
import java.util.Arrays;
public class FibonacciSearch {
public static int maxSize = 20;
public static void main(String[] args) {
int[] arr = {1, 8, 10, 89, 1000};
//int[] arr2 = {1, 2, 3, 4, 5};
System.out.println(fibSearch(arr, 1000));
//System.out.println(fibSearch(arr2, 2));
}
//非递归方法得到一个斐波那契数列 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;
}
/**
* @param arr 有序数组
* @param key 值
* @return 返回对应下标,没有则返回-1
*/
public static int fibSearch(int[] arr, int key) {
int low = 0;
int high = arr.length - 1;
int k = 0;//表示斐波那契分割数值的下标
int mid = 0;
int[] f = fib();
//
/*
* !!!【【high >= f[k] - 1】】
* !!!【【arr.length > f[k] - 1】】
* */
while (arr.length > f[k] - 1) {//获取到斐波那契分割数值的下标
k++;
}
//f[k]值可能大于arr长度,需要构造一个新数组指向arr[]
/*
* f[k]:数组个数
* f[k]-1:数组下标
* */
int[] temp = Arrays.copyOf(arr, f[k]);
/*
* 如果f[k]>arr.length,则temp={1,8,10,89,1000,1234,0,0}
* ===>temp={1,8,10,89,1000,1234,1234,1234}
* */
for (int i = high + 1; i < temp.length; i++) {
temp[i] = arr[high];
}
while (low <= high) {
mid = low + f[k - 1] - 1;
if (key < temp[mid]) {//向数组前面(左)查找
high = mid - 1;
k--;
/*
* k--?
* 全部元素个数=前面元素个数+后面元素个数
* f[k] = f[k-1] + f[k-2]
* 数组前半部分f[k-1]还可以写成f[k-1]=f[k-2]+f[k-3]
* 即:在f[k-1]的前半部分继续查找
* 所以是k--
* 即下次循环mid = low + f[k-1-1]-1
* */
} else if (key > temp[mid]) {//向数组后面(右)查找
low = mid + 1;
k -= 2;
/*
* k-=2?
* 全部元素个数=前面元素个数+后面元素个数
* f[k] = f[k-1] + f[k-2]
* 数组后半部分f[k-2]还可以写成f[k-2]=f[k-3]+f[k-4]
* 即:在f[k-1]的后半部分继续查找
* 所以是k-=2
* 即下次循环mid = low + f[k-1-2]-1
* */
} else {//找到了
//temp数组补全过,比arr数组长度大
if (mid <= high) {
return mid;
} else {
//temp后面可能多出来几个和high一样的元素,返回mid的话超过arr数组长度了
return high;
}
}
}
return -1;
}
}
八、哈希表
JDK7:数组+链表
JDK8:数组+链表+红黑树
8.1 基本思路
通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。映射函数叫做散列函数,存放记录的数组叫做散列表。
8.2 图解
8.3 代码演示
添加、查找、删除、遍历员工信息(根据id),用链表实现哈希表,链表不带表头(即: 链表的第一个结点就直接存放雇员信息)
public class Emp {
public int id;
public String name;
public Emp next;//默认为空
public Emp(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "Emp{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
public class EmpLinkedList {
//头指针,指向第一个Emp
private Emp head;//默认为null
//添加
public void add(Emp emp) {
if (head == null) {//第一个
head = emp;
return;
}
Emp temp = head;//辅助节点
while (true) {
if (temp.next == null) {
break;
}
temp = temp.next;
}
temp.next = emp;
}
//遍历
public void list(int no) {
if (head == null) {
System.out.println("第" + no + "条链表为空");
return;
}
System.out.println("第" + no + "条链表信息为:");
Emp temp = head;//辅助节点
while (true) {
System.out.print(temp + "\t");
if (temp.next == null) {
break;
}
temp = temp.next;
}
/*while (temp != null) {
System.out.println(temp);
temp = temp.next;
}*/
System.out.println();
}
//根据id查找
public Emp findEmpById(int id) {
if (head == null) {
System.out.println("链表为空");
return null;
}
Emp temp = head;
while (true) {
if (temp.id == id) {//找到
break;
}
//退出
if (temp.next == null) {//未找到
temp = null;
break;
}
temp = temp.next;
}
return temp;
}
//根据id删除
public boolean deleteById(int id) {
if(head==null){
System.out.println("链表为空");
return false;
}
if (findEmpById(id) == null) {
System.out.println("未找到"+id+"号雇员");
return false;
}
Emp temp = head;//辅助节点
boolean flag = false;//表示是否找到待删除节点的前一个节点
while (true) {
if (temp == null) {
break;
}
if (temp.id == id) {
flag = true;
break;
}
temp = temp.next;
}
if(flag){
temp.next = temp.next.next;
return true;
}else{
return false;
}
}
}
//创建HashTable,管理多条链表
public class HashTab {
private EmpLinkedList[] empLinkedListArray;
private int size;//多少条链表
//构造器
public HashTab(int size) {
this.size = size;
empLinkedListArray = new EmpLinkedList[size];
//!【必须初始化链表数组中的每一条链表】否则会空指针异常
for (int i = 0; i < size; i++) {
empLinkedListArray[i] = new EmpLinkedList();
}
}
//添加雇员
public void add(Emp emp) {
//根据员工id,得到该员工该添加到哪条链表
int empLinkedListNo = hashFun(emp.id);
//将emp添加到对应的链表中
empLinkedListArray[empLinkedListNo].add(emp);
}
//遍历所有的链表
public void list(){
for (int i = 0; i < size; i++) {
empLinkedListArray[i].list(i);
}
}
//根据输入id查找
public void findEmpById(int id){
int empLinkedListNo = hashFun(id);
Emp emp = empLinkedListArray[empLinkedListNo].findEmpById(id);
if(emp!=null){
System.out.println("在第"+empLinkedListNo+"条链表中找到雇员"+id);
}else{
System.out.println("未找到");
}
}
//根据id删除
public void deleteById(int id){
int empLinkedListNo = hashFun(id);
boolean flag = empLinkedListArray[empLinkedListNo].deleteById(id);
if(flag){
System.out.println("第"+empLinkedListNo+"条链表中雇员"+id+"已删除");
}else{
System.out.println("删除失败");
}
}
//编写散列函数,使用简单的取模法
public int hashFun(int id) {
return id % size;
}
}
import java.util.Scanner;
public class HashTableDemo {
public static void main(String[] args) {
//创建哈希表
HashTab hashTab = new HashTab(7);
String key = "";
Scanner sc = new Scanner(System.in);
while(true){
System.out.println("add:添加雇员\nlist:显示雇员\nfind:查找雇员\ndelete:删除雇员\nexit:退出");
key = sc.next();
switch (key){
case"add":
System.out.println("请输入id:");
int id = sc.nextInt();
System.out.println("请输入名字:");
String name = sc.next();
hashTab.add(new Emp(id,name));
break;
case"list":
hashTab.list();
break;
case"find":
System.out.println("请输入id");
id = sc.nextInt();
hashTab.findEmpById(id);
break;
case"delete":
System.out.println("请输入id");
id = sc.nextInt();
hashTab.deleteById(id);
break;
case"exit":
sc.close();
System.exit(0);
break;
}
}
}
}
九、树
9.1 二叉树
满二叉树一定是完全二叉树,反之不一定。
9.2 二叉树性质
9.3 二叉树遍历
9.4 赫夫曼树
9.5 二叉排序树 (BST)
二叉排序树【中序遍历非空二叉排序树会得到一个递增有序的序列】(Binary Search Tree),又称二叉查找树。它是一棵空树或者具有下列性质的二叉树:
- 若它的左子树不为空,则左子树上所有结点的值均小于它的根结构的值
- 若它的右子树不为空,则右子树上所有结点的值均大于它的根结构的值
- 它的左右子树也分别是二叉排序树
构造二叉排序树的过程实际上也是一个排序的过程。中序遍历一棵二叉排序树会得到一个递增有序的序列。
- 被删除是叶子结点:直接删除该叶子结点
- 被删除结点只有左子树或者只有右子树:用其左子树或者右子树替换它(结点替换)
- 被删除结点既有左子树也有右子树:
- 用其中序遍历的前驱值替换(值替换),然后再删除该前驱结点。前驱是左子树中最大的结点。
- 用其中序遍历的后继值替换(值替换),然后再删除该后继结点。后继是右子树中最小的结点。
9.6 平衡二叉树(AVL)
通过二叉排序树的性质(一个结点的左子树小于它本身,右子树大于它本身)可知A、B和C三个结点的大小顺序,从而调整至平衡!
RL型调整过程:
- C结点穿过A、B上升
- A结点成为C的左孩子,B结点成为C的右孩子
- 原来C结点的左子树β作为A的右子树
- 原来C结点的右子树γ作为B的左子树
9.7 多路查找树
9.7.1 2-3树
9.7.2 2-3-4树
9.7.3 B树
B树的插入与删除,方式与2-3树和2-3-4数相似,只不过阶数大。
9.7.4 B+树