数据结构和算法
1. 数据结构
1.1 稀疏数组
- 这个简单
- 稀疏数组即二维数组中有大量为0或同一个无效值的时候,将其压缩为只有有效数据的稀疏数组,需要使用时将其读写出来转为二维数组。
public class dom1 {
public static void main(String[] args) {
//创建一个原始二维数组 11*11
int ints[][] = new int[11][11];
ints[1][2] = 1 ;
ints[2][3] = 2 ;
System.out.println("原始的二位数组");
for (int[] anInt : ints) {
for (int i : anInt) {
System.out.printf("%d\t",i);
}
System.out.println("");
}
//将二维数组转稀疏数组
//遍历二位数组,得到有效数据的个数
int sum = 0;
for (int[] anInt : ints) {
for (int i : anInt) {
if(i!=0){
sum++;
}
}
}
System.out.println("有效数据个数"+sum);
System.out.println("遍历完毕");
//创建稀疏数组
int arr[][] = new int[sum+1][3];
//给稀疏数组赋值
arr[0][2] = sum;
int count = 1; //记录第几个非零数
int col = 0;
for (int[] anInt : ints) {
for (int i : anInt) {
if(i!=0){
arr[count][0] = count; // 第几行
arr[count][1] = col; // 第几列
arr[count][2] = i;
count++;
continue;
}
col++;
}
col = 0;
}
//懒得加参数算行列个数了,就按照标准稀疏数组读取
arr[0][0] = 11;
arr[0][1] = 11;
//输出稀疏数组
/**
for (int[] ints1 : arr) {
for (int i : ints1) {
System.out.printf("%d\t",i);
}
System.out.println("");
}
*/
//稀疏数组只有三列,对以上打印方法优化
for(int i = 0;i < arr.length;i++){
System.out.printf("%d%d%d\n",arr[i][0],arr[i][1],arr[i][2]);
}
//将稀疏数组恢复成原始二位数组
//读取第一行,创建新二维数组
int[][] brr = new int[arr[0][0]][arr[0][1]];
//数据转移
int num2 = 0;
for (int i = 1;i<arr.length;i++){
brr[arr[i][0]][arr[i][1]] = arr[i][2];
}
//打印新二维数组
for (int[] ints1 : brr) {
for (int i : ints1) {
System.out.printf("%d\t",i);
}
System.out.println("");
}
}
}
1.2 队列
1.2.1 数组模拟单向队列
- 先来先出原则
- 代码实现:
import java.util.Scanner;
public class dom2 {
//队列本身是有序列表
public static void main(String[] args) {
ArrayQueue arrayQueue = new ArrayQueue(3);
//不添加先测展示数据
// arrayQueue.showQueue();
// System.out.println(arrayQueue.headQueue());
//搞一个菜单玩
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 'e':
//断开
scanner.close();
loop = false;
break;
case 'a':
System.out.println("客官想添点啥呢");
int value = scanner.nextInt();
arrayQueue.addQueue(value);
break;
case 'g':
//方法有异常处理,用try/catch写法
try {
System.out.println("提取出来的是"+arrayQueue.getQueue());
}catch (Exception e){
//固打印提升信息就行,别断进程
System.out.println(e.getMessage());
}
break;
case 'h':
try {
System.out.println("提取出来的队列头数据是"+arrayQueue.headQueue());
}catch (Exception e){
System.out.println(e.getMessage());
}
default:
//用户不按照菜单提示输入时...
System.out.println("给朕好好输!");
break;
}
}
System.out.println("欢迎下次光临~~~");
}
}
//数组模拟单向队列类
class ArrayQueue{
//最大容量
private int maxSize;
//队列头
private int front;
//队列尾
private int rear;
//模拟队列的数组
private int arr [];
//创建队列构造器
public ArrayQueue(int MaxSize){
maxSize = MaxSize;
arr = new int[maxSize];
//初始化指向队列头尾部前一个位置
front = -1;
rear = -1;
}
//判断队列满不满
public boolean isFull(){
return rear==maxSize-1?true:false;
}
//判断队列是不是空
public boolean isEmpty(){
return rear == front?true:false;
}
//添加数据到队列
public void addQueue(int n){
//判断是否满
if (isFull()){
System.out.println("满了满了满了满了!!!!!");
return;
}
rear++;
//添加是往尾部加
arr[rear] = n;
//简写:arr[++rear] = n;
}
//获取队列数据---出去
public int getQueue(){
//判断是否空
if (isEmpty()){
//通过异常处理
throw new RuntimeException("没了没了没了没了!!!!!");
//return; 这个不用写!!!
}
//出队列
front++;
return arr[front];
//return arr[++front];
}
//显示队列所有数据
public void showQueue(){
if (isEmpty()){
System.out.println("这啥也没有,拜拜了您呢");
return;
}
for (int i : arr) {
System.out.println(i);
}
}
//显示头数据
public int headQueue(){
if (isEmpty()){
throw new RuntimeException("想不到吧,队列是空的~~~~~~");
}
return arr[front+1];
//这不能写arr[0],即要返回的是当前还在排队的第一个
}
}
- 问题提出:数组只能单次使用,不能复用。
1.2.2 数组模拟环形队列
分析
- 解决单向队列不能复用的问题
- 满了的情况尾跑到头:rear代表指向元素最后一个元素,不是数组空间的固定某个位置
- 满了的条件:(rear+1)%maxSize = front; ---- 这个算法牛逼!
- 空的条件不变:rear = front
- 初始值:rear,front都是0;原先是站在队列外等待进去,现在是站在环形跑道内。
- 有效数据个数:(rear+maxSize-front)%maxSize
代码实现
- 在单项队列上进行改进
import java.util.Scanner;
/**
* 里面需要好好思考的算法:
* 判满:(rear+1)%maxSize==front?true:false;
*
* 有效值个数:(rear + maxSize -front)%maxSize;
*
* 巧用临时变量保存数据: int value = arr[front];
* front = (front+1)%maxSize;
* return value;
*
*打印全部有效数据: for (int i = front;i < front+size();i++){
* System.out.println(arr[i%maxSize]);
* }
*/
//环形队列
public class dom3 {
public static void main(String[] args) {
//测试环形队列
CircleQueue circleQueue = new CircleQueue(4);//队列有效数据是三个
//搞一个菜单玩
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':
circleQueue.showQueue();
break;
case 'e':
//断开
scanner.close();
loop = false;
break;
case 'a':
System.out.println("客官想添点啥呢");
int value = scanner.nextInt();
circleQueue.addQueue(value);
break;
case 'g':
//方法有异常处理,用try/catch写法
try {
System.out.println("提取出来的是"+circleQueue.getQueue());
}catch (Exception e){
//固打印提升信息就行,别断进程
System.out.println(e.getMessage());
}
break;
case 'h':
try {
System.out.println("提取出来的队列头数据是"+circleQueue.headQueue());
}catch (Exception e){
System.out.println(e.getMessage());
}
break;
default:
//用户不按照菜单提示输入时...
System.out.println("给朕好好输!");
break;
}
}
System.out.println("欢迎下次光临~~~");
}
}
//数组模拟环形队列类
class CircleQueue{
//最大容量
private int maxSize;
//队列头
private int front;
//队列尾
private int rear;
//模拟队列的数组
private int arr [];
//创建队列构造器
public CircleQueue(int MaxSize){
maxSize = MaxSize;
arr = new int[maxSize];
//初始化指向队列头尾部前一个位置
//rear,front默认为0,这里可以不用赋值
}
//判断队列满不满
public boolean isFull(){
return (rear+1)%maxSize==front?true:false;
}
//判断队列是不是空
public boolean isEmpty(){
return rear == front?true:false;
}
//添加数据到队列
public void addQueue(int n){
//判断是否满
if (isFull()){
System.out.println("满了满了满了满了!!!!!");
return;
}
arr[rear] = n;
//尾后移,防止越界
rear = (rear+1)%maxSize;
}
//获取队列数据---出去
public int getQueue(){
//判断是否空
if (isEmpty()){
//通过异常处理
throw new RuntimeException("没了没了没了没了!!!!!");
//return; 这个不用写!!!
}
//出队列
//return arr[front];
//不能直接返回,return后front就没有后移的机会了
//用临时变量保存起来,先把指针后移,再提取
//防止越界一定要取模
int value = arr[front];
front = (front+1)%maxSize;
return value;
}
//获取队列有效值个数
public int size(){
return (rear + maxSize -front)%maxSize;
}
//显示队列所有数据
public void showQueue(){
if (isEmpty()){
System.out.println("这啥也没有,拜拜了您呢");
return;
}
//从front遍历,遍历多少个元素。
for (int i = front;i < front+size();i++){
System.out.println(arr[i%maxSize]);
}
}
//显示头数据
public int headQueue(){
if (isEmpty()){
throw new RuntimeException("想不到吧,队列是空的~~~~~~");
}
return arr[front];
//这不能写arr[0],即要返回的是当前还在排队的第一个
}
}
1.3 链表
1.3.1 单链表的创建和遍历
基本介绍
-
链表是有序列表
-
链表以节点的方式存储
-
每个节点包含data域存储数据,next域指向下一个节点
-
链表的各个节点不一定是连续存储
-
链表分为带头结点的跟不带头节点的,头节点根据需求确定有无
-
头节点不存放具体数据,只存放指向下一个节点地址
-
最后一个节点next域为null
-
创建:
-
- 创建一个head头节点
- 每添加一个节点,直接加入到最后
-
遍历:
-
- 通过一个辅助变量,帮助遍历整个链表
代码实现----添加水浒人物
public class dom4 {
public static void main(String[] args) {
//先创建节点
HeroNode heroNode1 = new HeroNode(1, "宋江", "马后炮");
HeroNode heroNode2 = new HeroNode(2, "卢俊义", "黑麒麟");
HeroNode heroNode3 = new HeroNode(3, "吴用", "无用");
HeroNode heroNode4 = new HeroNode(4, "林冲", "猫猫头");
HeroNode heroNode5 = new HeroNode(5, "李逵", "李白的黑妹");
HeroNode heroNode6 = new HeroNode(6, "张顺", "浪里白条鸡");
HeroNode heroNode7 = new HeroNode(7, "孙二娘", "母老虎");
//new 一个管理器
SingleLinkedList singleLinkedList = new SingleLinkedList();
//加入
singleLinkedList.addSingleLinkedList(heroNode1);
singleLinkedList.addSingleLinkedList(heroNode2);
singleLinkedList.addSingleLinkedList(heroNode3);
singleLinkedList.addSingleLinkedList(heroNode4);
singleLinkedList.addSingleLinkedList(heroNode5);
singleLinkedList.addSingleLinkedList(heroNode6);
singleLinkedList.addSingleLinkedList(heroNode7);
//显示
singleLinkedList.list();
}
}
//定义SingleLinkedList管理hero
class SingleLinkedList{
//初始化头节点,头节点不动,不放具体数据
private HeroNode head = new HeroNode(0,"","");
//添加节点向单链表
//不考虑编号顺序时,找到当前链表最后节点,后节点next指向新节点
public void addSingleLinkedList(HeroNode heroNode){
//head节点不能动,需要辅助变量,注意辅助变量temp是HeroNode类
HeroNode temp = head;
//遍历链表找到最后
while (true){
if(temp.next == null){
break;
}
//没有找到null,temp后移
temp = temp.next;
}
//退出while循环时,temp指向了链表最后
//将最后节点指向新节点
temp.next = heroNode; // 所有数据都在next里面!!!!!!!!!!!!!!!!!!!将next想成层层嵌套的宝塔肉
}
//显示链表,通过辅助变量
public void list(){
//判断链表为空
if(head.next==null){
System.out.println("链表为空");
return;
}
//辅助变量遍历打印
HeroNode temp = head.next;
while (true){
//判断是否结束
if (temp == null){
break;
}
//输出节点信息
System.out.println(temp);
//一定要将next后移
temp = temp.next;
}
}
}
//定义一个HeroNode,每个对象是一个节点
class HeroNode{
private int no;
private String name;
private String nikeNode;
//数据都放在next里面
public HeroNode next;
//这样构造是为了插入数据
public HeroNode(int hno,String hName,String hNikeName){
this.no = hno;
this.name = hName;
this.nikeNode = hNikeName;
}
//显示方便,重写toString
//注意不要打印next,每一层next都包含着本层后面所有数据
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nikeNode='" + nikeNode +
'}';
}
}
-
当前实现的问题是将插入的数据连接起来,跟插入的前后有关,与插入的对象自身属性无关
-
与数组形式的列表不同,链表可以没有上限,真实物理内存动态变化。
-
这里较为难理解的next,注意!!!
-
只能实现尾插
1.3.2 单链表按顺序插入节点
- 修改addSingleLinkedList方法变成按顺序插入
public class dom4 {
public static void main(String[] args) {
//先创建节点
HeroNode heroNode1 = new HeroNode(1, "宋江", "马后炮");
HeroNode heroNode2 = new HeroNode(2, "卢俊义", "黑麒麟");
HeroNode heroNode5 = new HeroNode(5, "李逵", "李白的黑妹");
HeroNode heroNode6 = new HeroNode(6, "张顺", "浪里白条鸡");
HeroNode heroNode3 = new HeroNode(3, "吴用", "无用");
HeroNode heroNode4 = new HeroNode(4, "林冲", "猫猫头");
HeroNode heroNode7 = new HeroNode(7, "孙二娘", "母老虎");
//new 一个管理器
SingleLinkedList singleLinkedList = new SingleLinkedList();
//加入
singleLinkedList.addSingleLinkedList(heroNode1);
singleLinkedList.addSingleLinkedList(heroNode2);
singleLinkedList.addSingleLinkedList(heroNode3);
singleLinkedList.addSingleLinkedList(heroNode4);
singleLinkedList.addSingleLinkedList(heroNode5);
singleLinkedList.addSingleLinkedList(heroNode6);
singleLinkedList.addSingleLinkedList(heroNode7);
//显示
singleLinkedList.list();
}
}
//定义SingleLinkedList管理hero
class SingleLinkedList{
//初始化头节点,头节点不动,不放具体数据
private HeroNode head = new HeroNode(0,"","");
//添加节点向单链表
//不考虑编号顺序时,找到当前链表最后节点,后节点next指向新节点
public void addSingleLinkedList(HeroNode heroNode){
//head节点不能动,需要辅助变量
HeroNode temp = head;
//判断是不是已经有这个编号
boolean flag = false;
//遍历链表找到要插入的位置
while (true){
//temp在链表最后面
if(temp.next == null){
break;
}
//直接从头节点开始判断下一个与当前heroNode.no,这个真没想到
//排除法原则
if(temp.next.no > heroNode.no){
break;
}else if(temp.next.no == heroNode.no){
flag=true;
break;
}
temp = temp.next;
}
//判断flag
if(flag){
System.out.println(heroNode.no+" 这个编号被占了");
}else {
heroNode.next = temp.next;
temp.next = heroNode;
}
}
//显示链表,通过辅助变量
public void list(){
//判断链表为空
if(head.next==null){
System.out.println("链表为空");
return;
}
//辅助变量遍历打印
HeroNode temp = head.next;
while (true){
//判断是否结束
if (temp == null){
break;
}
//输出节点信息
System.out.println(temp);
//一定要将next后移
temp = temp.next;
}
}
}
//定义一个HeroNode,每个对象是一个节点
class HeroNode{
public int no;
//这俩参数暂时用不到
private String name;
private String nikeNode;
//数据都放在next里面
public HeroNode next;
//这样构造是为了插入数据
public HeroNode(int hno,String hName,String hNikeName){
this.no = hno;
this.name = hName;
this.nikeNode = hNikeName;
}
//显示方便,重写toString
//注意不要打印next,每一层next都包含着本层后面所有数据
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nikeNode='" + nikeNode +
'}';
}
}
- 算法亮点:用链表的机制做插入判断,遍历过程中直接判断下一个。
1.3.3 单链表节点的修改
public class dom4 {
public static void main(String[] args) {
//先创建节点
HeroNode heroNode1 = new HeroNode(1, "宋江", "马后炮");
HeroNode heroNode2 = new HeroNode(2, "卢俊义", "黑麒麟");
HeroNode heroNode5 = new HeroNode(5, "李逵", "李白的黑妹");
HeroNode heroNode6 = new HeroNode(6, "张顺", "浪里白条鸡");
HeroNode heroNode3 = new HeroNode(3, "吴用", "无用");
HeroNode heroNode4 = new HeroNode(4, "林冲", "猫猫头");
HeroNode heroNode7 = new HeroNode(7, "孙二娘", "母老虎");
//测试修改
HeroNode heroNode8 = new HeroNode(1, "鲁智深", "阿弥陀佛么么哒");
HeroNode heroNode9 = new HeroNode(3, "武松", "动物保护协会会长");
HeroNode heroNode10 = new HeroNode(7, "史进", "纹身的不良少年");
HeroNode heroNode11 = new HeroNode(8, "关胜", "关二爷的小迷弟");
//new 一个管理器
SingleLinkedList singleLinkedList = new SingleLinkedList();
//加入
singleLinkedList.addSingleLinkedList(heroNode1);
singleLinkedList.addSingleLinkedList(heroNode2);
singleLinkedList.addSingleLinkedList(heroNode3);
singleLinkedList.addSingleLinkedList(heroNode4);
singleLinkedList.addSingleLinkedList(heroNode5);
singleLinkedList.addSingleLinkedList(heroNode6);
singleLinkedList.addSingleLinkedList(heroNode7);
//修改
singleLinkedList.updateSingleLinkedList(heroNode8);
singleLinkedList.updateSingleLinkedList(heroNode9);
singleLinkedList.updateSingleLinkedList(heroNode10);
//测试不存在的对象修改
singleLinkedList.updateSingleLinkedList(heroNode11);
//显示
singleLinkedList.list();
}
}
//定义SingleLinkedList管理hero
class SingleLinkedList{
//初始化头节点,头节点不动,不放具体数据
private HeroNode head = new HeroNode(0,"","");
//添加节点向单链表
//不考虑编号顺序时,找到当前链表最后节点,后节点next指向新节点
public void addSingleLinkedList(HeroNode heroNode){
//head节点不能动,需要辅助变量
HeroNode temp = head;
//判断是不是已经有这个编号
boolean flag = false;
//遍历链表找到要插入的位置
while (true){
//temp在链表最后面
if(temp.next == null){
break;
}
//直接从头节点开始判断下一个与当前heroNode.no,这个真没想到
//排除法原则
if(temp.next.no > heroNode.no){
break;
}else if(temp.next.no == heroNode.no){
flag=true;
break;
}
temp = temp.next;
}
//判断flag
if(flag){
System.out.println(heroNode.no+" 这个编号被占了");
}else {
heroNode.next = temp.next;
temp.next = heroNode;
}
}
//显示链表,通过辅助变量
public void list(){
//判断链表为空
if(head.next==null){
System.out.println("链表为空");
return;
}
//辅助变量遍历打印
HeroNode temp = head.next;
while (true){
//判断是否结束
if (temp == null){
break;
}
//输出节点信息
System.out.println(temp);
//一定要将next后移
temp = temp.next;
}
}
//根据newHeroNode的 no 修改就行
public void updateSingleLinkedList(HeroNode newHeroNode){
if (head.next == null){
System.out.println("链表是空哒小老弟");
return;
}
HeroNode temp = head;
while (true){
if(temp == null){
System.out.println("没有 "+newHeroNode.no+" 号这个小朋友");
break;
}
//找到了
if(temp.no == newHeroNode.no){
temp.name = newHeroNode.name;
temp.nikeNode = newHeroNode.nikeNode;
break;
}
temp = temp.next;
}
}
}
//定义一个HeroNode,每个对象是一个节点
class HeroNode{
public int no;
//能修改了那就改成public
public String name;
public String nikeNode;
//数据都放在next里面
public HeroNode next;
//这样构造是为了插入数据
public HeroNode(int hno,String hName,String hNikeName){
this.no = hno;
this.name = hName;
this.nikeNode = hNikeName;
}
//显示方便,重写toString
//注意不要打印next,每一层next都包含着本层后面所有数据
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nikeNode='" + nikeNode +
'}';
}
}
- 加一个update类方法就行,这个简单
1.3.4 单链表节点的删除
public class dom4 {
public static void main(String[] args) {
//先创建节点
HeroNode heroNode1 = new HeroNode(1, "宋江", "马后炮");
HeroNode heroNode2 = new HeroNode(2, "卢俊义", "黑麒麟");
HeroNode heroNode5 = new HeroNode(5, "李逵", "李白的黑妹");
HeroNode heroNode6 = new HeroNode(6, "张顺", "浪里白条鸡");
HeroNode heroNode3 = new HeroNode(3, "吴用", "无用");
HeroNode heroNode4 = new HeroNode(4, "林冲", "猫猫头");
HeroNode heroNode7 = new HeroNode(7, "孙二娘", "母老虎");
//测试修改
HeroNode heroNode8 = new HeroNode(1, "鲁智深", "阿弥陀佛么么哒");
HeroNode heroNode9 = new HeroNode(3, "武松", "动物保护协会会长");
HeroNode heroNode10 = new HeroNode(7, "史进", "纹身的不良少年");
HeroNode heroNode11 = new HeroNode(8, "关胜", "关二爷的小迷弟");
//new 一个管理器
SingleLinkedList singleLinkedList = new SingleLinkedList();
//增加
singleLinkedList.addSingleLinkedList(heroNode1);
singleLinkedList.addSingleLinkedList(heroNode2);
singleLinkedList.addSingleLinkedList(heroNode3);
singleLinkedList.addSingleLinkedList(heroNode4);
singleLinkedList.addSingleLinkedList(heroNode5);
singleLinkedList.addSingleLinkedList(heroNode6);
singleLinkedList.addSingleLinkedList(heroNode7);
//修改
singleLinkedList.updateSingleLinkedList(heroNode8);
singleLinkedList.updateSingleLinkedList(heroNode9);
singleLinkedList.updateSingleLinkedList(heroNode10);
//测试不存在的对象修改
singleLinkedList.updateSingleLinkedList(heroNode11);
//删除
singleLinkedList.deleteSingleLinkedList(3);
singleLinkedList.deleteSingleLinkedList(7);
singleLinkedList.deleteSingleLinkedList(9);
//显示
singleLinkedList.list();
}
}
//定义SingleLinkedList管理hero
class SingleLinkedList{
//初始化头节点,头节点不动,不放具体数据
private HeroNode head = new HeroNode(0,"","");
//添加节点向单链表
//不考虑编号顺序时,找到当前链表最后节点,后节点next指向新节点
public void addSingleLinkedList(HeroNode heroNode){
//head节点不能动,需要辅助变量
HeroNode temp = head;
//判断是不是已经有这个编号
boolean flag = false;
//遍历链表找到要插入的位置
while (true){
//temp在链表最后面
if(temp.next == null){
break;
}
//直接从头节点开始判断下一个与当前heroNode.no,这个真没想到
//排除法原则
if(temp.next.no > heroNode.no){
break;
}else if(temp.next.no == heroNode.no){
flag=true;
break;
}
temp = temp.next;
}
//判断flag
if(flag){
System.out.println(heroNode.no+" 这个编号被占了");
}else {
heroNode.next = temp.next;
temp.next = heroNode;
}
}
//显示链表,通过辅助变量
public void list(){
//判断链表为空
if(head.next==null){
System.out.println("链表为空");
return;
}
//辅助变量遍历打印
HeroNode temp = head.next;
while (true){
//判断是否结束
if (temp == null){
break;
}
//输出节点信息
System.out.println(temp);
//一定要将next后移
temp = temp.next;
}
}
//根据newHeroNode的 no 修改就行
public void updateSingleLinkedList(HeroNode newHeroNode){
if (head.next == null){
System.out.println("链表是空哒小老弟");
return;
}
HeroNode temp = head;
while (true){
if(temp == null){
System.out.println("没有 "+newHeroNode.no+" 号这个小朋友");
break;
}
//找到了
if(temp.no == newHeroNode.no){
temp.name = newHeroNode.name;
temp.nikeNode = newHeroNode.nikeNode;
break;
}
temp = temp.next;
}
}
//删除,想删几号嘉宾?
public void deleteSingleLinkedList(int no){
if (head.next == null){
System.out.println("链表是空哒小老弟");
return;
}
HeroNode temp = head;
//自己写的,绕了点
// while (true){
// //尾删,不管咋删注意遍历的temp指向的是欲删节点的前一个节点
// if(temp.next.no == no && temp.next.next==null){
// temp.next = null;
// System.out.println("删除了"+ no +"号嘉宾");
// break;
// }
// //尾之前的删
// if(temp.next.no == no){
// temp.next = temp.next.next;
// System.out.println("删除了"+ no +"号嘉宾");
// break;
// }
// temp = temp.next;
// //注意判断遍历完的位置
// if(temp.next == null){
// System.out.println("没有 "+ no +" 号这个小朋友");
// break;
// }
// }
//大佬的思维
//先判断能不能删!默认不能
boolean flag = false;
while (true){
if(temp.next == null){
System.out.println("没有 "+no+" 号这个小朋友");
break;
}
if(temp.next.no == no){
flag = true;
break;
}
temp = temp.next;
}
if (flag){
temp.next = temp.next.next;
}
}
}
//定义一个HeroNode,每个对象是一个节点
class HeroNode{
public int no;
//能修改了那就改成public
public String name;
public String nikeNode;
//数据都放在next里面
public HeroNode next;
//这样构造是为了插入数据
public HeroNode(int hno,String hName,String hNikeName){
this.no = hno;
this.name = hName;
this.nikeNode = hNikeName;
}
//显示方便,重写toString
//注意不要打印next,每一层next都包含着本层后面所有数据
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nikeNode='" + nikeNode +
'}';
}
}
-
被删除的节点不会有任何引用指向它,会被jvm垃圾回收机制清理。
-
回顾单链表:
-
- 链表存储的形式
- 逻辑示意图-像羊肉串
- 链表的增删改查
- 大佬的思维:修改和删除–标志位法,先找到该节点(删除-节点前一项)
1.3.5 单链表经典面试题
- 求单链表中节点个数
- 查找单链表倒数第k个节点 – 新浪
- 单链表反转 – 腾讯
- 从尾到头打印单链表 --要求:方法1.反向遍历 方法2.Stack栈 – 百度
- 合并两个有序链表,合并之后的链表依然有序
-
尝试实现-
-
方便显示五道题互不干涉,实现的方法写在一起
-
- 实现成功:
-
- 计算节点个数: T(n) = n
-
- 查找单链表倒数第k个节点: T(n) = n
-
- 从尾到头打印单链表: T(n) = n2
-
- 未实现成功
-
- 反转 — 不了解栈的具体操作实现,暴力遍历…
- 欠缺思路
-
- 合并
public class dom5 {
public static void main(String[] args) {
//创建对象
User u1 = new User(1, "赵信");
User u2 = new User(2, "盖伦");
User u3 = new User(3, "嘉文");
User u4 = new User(4, "铁苍蝇");
User u5 = new User(5, "狗头");
//创建链表管理器
SingleLinkedList2 singleLinkedList2 = new SingleLinkedList2();
//测试添加
singleLinkedList2.addUser(u1);
singleLinkedList2.addUser(u4);
singleLinkedList2.addUser(u2);
singleLinkedList2.addUser(u3);
//测试显示
singleLinkedList2.showUser();
//测试计数器
System.out.println(singleLinkedList2.UserNum());
//测试倒数第 k 个数据打印
singleLinkedList2.reciprocalByK(1);
singleLinkedList2.reciprocalByK(2);
singleLinkedList2.reciprocalByK(3);
singleLinkedList2.reciprocalByK(4);
singleLinkedList2.reciprocalByK(5);
singleLinkedList2.reciprocalByK(-1);
//先测尾插---添加时候只能添加新的节点对象,不能重复添加,重复的链表对象只要不是尾插next里面不是null,容易造成死循环
singleLinkedList2.add(u5);
//测试反转---测试失败!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//singleLinkedList2.reversal();
//System.out.println("----------------");
//singleLinkedList2.showUser();
//测试反向打印
singleLinkedList2.reciprocalPrintln();
}
}
class SingleLinkedList2{
//头节点
User head = new User(0,"");
//给尾插用(受反转影响)
User str = head;
//有序添加,自动排序
public void addUser(User user){
User temp = head;
//能不能插入,默认不能
boolean flag = false;
while (true) {
if(temp.next == null){
flag = true;
break;
}
if(temp.next.id>user.id){
flag = true;
break;
}else if(temp.next.id==user.id){
break;
}
temp = temp.next;
}
if(flag){
user.next = temp.next;
temp.next = user;
}else {
System.out.println(user.id+"数据重复,插入失败");
}
}
//尾插--反转用得到
public void add(User user){
while (true){
if(str.next == null){
break;
}
str = str.next;
}
str.next = user;
}
//显示所有数据
public void showUser(){
if(head.next==null){
System.out.println("链表为空");
return;
}
User temp = head.next;
while (true) {
if(temp == null){
break;
}
System.out.println(temp);
temp = temp.next;
}
}
//计算节点数
public int UserNum(){
if(head.next == null){
return 0;
}
User temp = head.next;
int con = 0;
while (true){
if(temp == null){
return con;
}
con++;
temp = temp.next;
}
}
//打印倒数第k个对象
public void reciprocalByK(int k){
if (head.next == null){
System.out.println("链表为空");
return;
}
if(k < 1){
System.out.println("无效参数");
return;
}
int num = UserNum();
if(k>num){
System.out.println("链表当前仅有 " + num +"条数据");
return;
}
User temp = head.next;
while (num-k > 0){
num--;
temp = temp.next;
}
System.out.println("倒数第 "+ k + " 条数据为" + temp);
}
//链表反转--先写的反向打印--反向都是一个思路 失败!!!!!!!!!!!!!
public void reversal(){
if (head.next == null){
System.out.println("链表为空");
return;
}
int num = UserNum();
//这里得用一个新临时头节点代表之前的链表头,真正的head得变了
User newHead = head.next;
User temp = head.next;
while (num>0){
int i = num;
while (true){
if(i == 1){
//调用尾插-- 置空,只插一条数据
User user = temp;
user.next = null;
add(user);
break;
}
i -- ;
temp = temp.next;
}
num --;
//头节点后移
head = head.next;
temp = newHead.next;
}
}
//链表反向打印---有UserNum干啥都方便
public void reciprocalPrintln(){
if (head.next == null){
System.out.println("链表为空");
return;
}
int num = UserNum();
User temp = head.next;
while (num>0){
int i = num;
while (true){
if(i == 1){
System.out.println(temp);
break;
}
i -- ;
temp = temp.next;
}
num --;
temp = head.next;
}
System.out.println("反向打印完毕");
}
}
//链表对象
class User{
public int id;
public String name;
public User next;
public User(int Uid , String Uname){
this.id = Uid;
this.name = Uname;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name +
'}';
}
}
- 来看大佬的思维
//单链表面试题
// > 1. 求单链表中节点个数
// > 2. 查找单链表倒数第k个节点 -- 新浪
// > 3. 单链表反转 -- 腾讯
// > 4. 从尾到头打印单链表 --要求:方法1.反向遍历 方法2.Stack栈 -- 百度
// > 5. 合并两个有序链表,合并之后的链表依然有序
import java.util.Stack;
public class dom5 {
public static void main(String[] args) {
//创建对象
User u1 = new User(1, "赵信");
User u2 = new User(2, "盖伦");
User u3 = new User(3, "嘉文");
User u4 = new User(4, "铁苍蝇");
User u5 = new User(5, "狗头");
//头节点
User head = new User(0,"");
//创建链表管理器
SingleLinkedList2 singleLinkedList2 = new SingleLinkedList2();
//测试添加
singleLinkedList2.addUser(head,u1);
singleLinkedList2.addUser(head,u4);
singleLinkedList2.addUser(head,u2);
singleLinkedList2.addUser(head,u3);
//测试显示
// singleLinkedList2.showUser(head);
//测试计数器
// System.out.println(singleLinkedList2.UserNum(head));
//测试倒数第 k 个数据打印---我的写法
// singleLinkedList2.reciprocalByK(head,1);
// singleLinkedList2.reciprocalByK(head,2);
// singleLinkedList2.reciprocalByK(head,3);
// singleLinkedList2.reciprocalByK(head,4);
// singleLinkedList2.reciprocalByK(head,5);
// singleLinkedList2.reciprocalByK(head,-1);
// System.out.println("倒数第k个节点,老师写法");
// User test1 = singleLinkedList2.find(head,1);
// User test2 = singleLinkedList2.find(head,2);
// User test3 = singleLinkedList2.find(head,3);
// User test4 = singleLinkedList2.find(head,4);
// User test5 = singleLinkedList2.find(head,5);
// System.out.println(test1);
// System.out.println(test2);
// System.out.println(test3);
// System.out.println(test4);
// System.out.println(test5);
//测试反转---测试失败!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//singleLinkedList2.reversal();
//System.out.println("----------------");
//singleLinkedList2.showUser();
//测试反转,新思路:从倒数第二个开始依次向前遍历添加到最后面
//测试反转---测试失败!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//System.out.println("------------------------");
//singleLinkedList2.reversal2(head);
//singleLinkedList2.showUser(head);
//失败总结:::永远不要在同一条链中有复制粘贴类的想法!!!!,它会出现同手同脚,删除时两个位置全部删,赋值死循环的问题。
//单链表反转---测试老师的代码
// System.out.println("原版:");
// singleLinkedList2.showUser(head);
// System.out.println("反转后");
// SingleLinkedList2.reversalList(head);
// singleLinkedList2.showUser(head);
//测试反向打印
//singleLinkedList2.reciprocalPrintln();
//测试stack栈的逆向打印方式,不会改变链表结构
System.out.println("原版:");
singleLinkedList2.showUser(head);
System.out.println("逆向打印");
SingleLinkedList2.reversePrint(head);
}
}
class SingleLinkedList2{
//有序添加,自动排序
public void addUser(User head,User user){
User temp = head;
//能不能插入,默认不能
boolean flag = false;
while (true) {
if(temp.next == null){
flag = true;
break;
}
if(temp.next.id>user.id){
flag = true;
break;
}else if(temp.next.id==user.id){
break;
}
temp = temp.next;
}
if(flag){
user.next = temp.next;
temp.next = user;
}else {
System.out.println(user.id+"数据重复,插入失败");
}
}
//尾插--反转用得到--实际上尾插可以,用尾插的反转没成功
public static void add(User head,User user){
User str = head;
while (true){
if(str.next == null){
break;
}
str = str.next;
}
str.next = user;
}
//显示所有数据
public void showUser(User head){
if(head.next==null){
System.out.println("链表为空");
return;
}
User temp = head.next;
while (true) {
if(temp == null){
break;
}
System.out.println(temp);
temp = temp.next;
}
}
//计算节点数
public static int UserNum(User head){
if(head.next == null){
return 0;
}
User temp = head.next;
int length = 0;
while (temp != null){
length ++;
temp = temp.next;
}
return length;
}
//打印倒数第k个对象
public static void reciprocalByK(User head,int k){
if (head.next == null){
System.out.println("链表为空");
return;
}
if(k < 1){
System.out.println("无效参数");
return;
}
int num = UserNum(head);
if(k>num){
System.out.println("链表当前仅有 " + num +"条数据");
return;
}
User temp = head.next;
while (num-k > 0){
num--;
temp = temp.next;
}
System.out.println("倒数第 "+ k + " 条数据为" + temp);
}
//打印第k个节点,老师写法
public static User find(User head,int index){
if(head.next == null){
return null;
}
//第一次遍历得到链表长度
int num = UserNum(head);
User temp = head.next;
if(index < 1 || index - num > 0){
System.out.println("无效参数");
return null;
}
while (num-index > 0){
num--;
temp = temp.next;
}
return temp;
}
//链表反转--先写的反向打印--反向都是一个思路
public static void reversal(User head) {
if (head.next == null){
System.out.println("链表为空");
return;
}
int num = UserNum(head);
//这里得用一个新临时头节点代表之前的链表头,真正的head得变了
User newHead = head.next;
User temp = head.next;
while (num>0){
int i = num;
while (true){
if(i == 1){
//调用尾插-- 置空,只插一条数据
User user = temp;
user.next = null;
add(head,user);
break;
}
i -- ;
temp = temp.next;
}
num --;
//头节点后移
head = head.next;
temp = newHead.next;
}
}
//链表反转--新思路--从倒数第二个开始依次向前遍历添加到最后面--遍历length-1次
public void reversal2(User head){
if(head.next == null){
System.out.println("链表为空");
return;
}
int length = UserNum(head)-1;
User temp = head;
while(length>0){
//临时存储数据
User sum;
//找到原链表倒数第二个数据
while (true){
if(temp.next.next.next == null){
sum = temp;
while (true){
if(temp.next == null){
temp.next = sum.next;
temp.next.next = null;
break;
}
temp = temp.next;
}
break;
}
temp = temp.next;
}
//删除原位置节点
sum.next = sum.next.next;
temp = head.next;
length--;
}
}
//关于反转看看老师的思路
public static void reversalList(User head){
//只有一个就没必要了
if(head.next == null || head.next.next == null){
return;
}
//定义辅助指针,帮助遍历原来链表
User cur = head.next;
//指向当前节点的下一个节点
User next = null;
//新的表头
User reverseHead = new User(0,"");
//遍历原来链表,每遍历一个节点将其取出,放在node前面
while (cur != null){
//暂时保存下一个节点
next = cur.next;
//将cur下一个节点指向新链表最前端--不要原链表后面的了!!
cur.next = reverseHead.next;
//将原链表连接到新链表
reverseHead.next = cur;
cur = next;
}
//连接
head.next = reverseHead.next;
}
//链表反向打印---有UserNum干啥都方便
public void reciprocalPrintln(User head){
if (head.next == null){
System.out.println("链表为空");
return;
}
int num = UserNum(head);
User temp = head.next;
while (num>0){
int i = num;
while (true){
if(i == 1){
System.out.println(temp);
break;
}
i -- ;
temp = temp.next;
}
num --;
temp = head.next;
}
System.out.println("反向打印完毕");
}
//反向打印:老师思维路线---1.先反转,再打印(时间复杂度为n,无需嵌套,完美方法),但是会破坏原链表结构,这个可以实现就不用了。
//2.利用栈数据结构,将各个节点压入到栈中,利用栈的先进后出特点。
public static void reversePrint(User head){
if(head.next == null || head.next.next == null){
return;
}
Stack<User> stack = new Stack<>();
User temp = head.next;
while (temp != null){
//入栈,先进
stack.push(temp);
temp = temp.next;
}
//出栈,后出
while (stack.size()>0){
System.out.println(stack.pop());
}
}
}
//链表对象
class User{
public int id;
public String name;
public User next;
public User(int Uid , String Uname){
this.id = Uid;
this.name = Uname;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name +
'}';
}
}
- 算法亮点及收获:
-
- 静态方法
- 代码更加简洁
- 反转:对同一链表不能做copy类行为,用一条新链表做临时变量,原表顺序遍历再到新表从前插队,时间复杂度 T(n) = n; 高效简单易读的算法!
- 反向打印:之前的算法反向遍历类似暴力排序,T(n) = n2; 不推荐使用,stack栈的操作真的是方便快捷。
1.3.6 双向链表
-
单链表体现出来的不足:
-
- 每次只能从头并且只有一个方向遍历,双向链表有两个方向
- 单链表删除只能依赖前一个节点,不能自我删除,双向链表可以实现自我删除
-
双向链表创建与基本增删改查功能实现
public class dom6 {
public static void main(String[] args) {
Student st1 = new Student(1, "张三");
Student st2 = new Student(2, "李四");
Student st3 = new Student(3, "王二");
Student st4 = new Student(4, "马六");
Student st5 = new Student(4, "老八");
Student st6 = new Student(1, "老八");
Student st7 = new Student(0, "老八");
Student st8 = new Student(5, "老八");
//插入
SingleLinkedList3 singleLinkedList3 = new SingleLinkedList3();
singleLinkedList3.addById(singleLinkedList3.getHead(), st1);
singleLinkedList3.addById(singleLinkedList3.getHead(), st3);
singleLinkedList3.addById(singleLinkedList3.getHead(), st4);
singleLinkedList3.addById(singleLinkedList3.getHead(), st2);
singleLinkedList3.list(singleLinkedList3.getHead());
//修改
// singleLinkedList3.update(singleLinkedList3.getHead(), st5);
// singleLinkedList3.update(singleLinkedList3.getHead(), st6);
// singleLinkedList3.update(singleLinkedList3.getHead(), st7);
// singleLinkedList3.update(singleLinkedList3.getHead(), st8);
// singleLinkedList3.list(singleLinkedList3.getHead());
//删除
// singleLinkedList3.delete(singleLinkedList3.getHead(),1);
// singleLinkedList3.delete(singleLinkedList3.getHead(),2);
// singleLinkedList3.delete(singleLinkedList3.getHead(),4);
// singleLinkedList3.delete(singleLinkedList3.getHead(),5);
// singleLinkedList3.list(singleLinkedList3.getHead());
}
}
//双向链表管理器类
class SingleLinkedList3{
Student head = new Student(0,"");
//获取头节点
public Student getHead(){
return head;
}
//遍历双向链表
public void list(Student head){
if(head.next == null){
System.out.println("链表为空");
}
Student temp = head.next;
while (temp != null){
System.out.println(temp);
temp = temp.next;
}
}
//尾插
public void add(Student head,Student student){
Student temp = head;
while (true){
if(temp.next == null){
temp.next = student;
student.pre = temp;
break;
}
temp = temp.next;
}
}
//添加--按顺序
public void addById(Student head , Student student){
Student temp = head;
//判断能不能插入,默认为true
boolean flag = true;
//找插入位置
while (true){
if(temp.next == null){
break;
}
if(temp.next.id > student.id){
break;
}else if(temp.next.id == student.id){
flag = false;
}
temp = temp.next;
}
if(flag){
student.next = temp.next;
temp.next = student;
student.pre = temp;
if(student.next != null){
student.next.pre = student;
}
}else {
System.out.println(student.id + ":该编号已存在");
}
}
//修改
public void update(Student head , Student student){
if(head.next == null){
System.out.println("链表为空");
return;
}
boolean flag = false;
Student temp = head.next;
while (true){
if(temp.id == student.id){
flag = true;
break;
}
if(temp.next == null){
break;
}
temp = temp.next;
}
if(flag){
temp.name = student.name;
}else {
System.out.println("查无此人");
}
}
//删除
public void delete(Student head , int index){
if(head.next == null){
System.out.println("链表为空");
return;
}
Student temp = head.next;
boolean flag = false;
while (true){
if(temp == null){
break;
}
if(temp.id == index){
if(temp.next == null){
temp.pre.next = null;
return;
}
flag = true;
break;
}
temp = temp.next;
}
if(flag){
temp.pre.next = temp.next;
temp.next.pre = temp.pre;
}else {
System.out.println("无效参数");
}
}
}
//双向链表类
class Student{
int id;
String name;
Student next;
Student pre;
public Student(int id,String name){
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
1.3.7 单向环形链表与约瑟夫问题
- Josephu 约瑟夫环
约瑟夫问题详情
-
编号为1,2,3…的n个人围坐在一圈,约定编号为k的人从1报数,数到m的人出列,以此类推,直到所有人出列,求产生的出队编号。
-
分析:单项环形链表!
-
先自己写一个,不难
public class dom7 {
public static void main(String[] args) {
SingleLinkedList4 singleLinkedList4 = new SingleLinkedList4();
//边界测试,边界值为 n>0,m>0
singleLinkedList4.GetJosepho(0,1);
singleLinkedList4.GetJosepho(1,0);
singleLinkedList4.GetJosepho(0,0);
singleLinkedList4.GetJosepho(1,1);
//测试约瑟夫问题
singleLinkedList4.GetJosepho(5,9);
singleLinkedList4.GetJosepho(10,9);
singleLinkedList4.GetJosepho(10,1);
}
}
class SingleLinkedList4{
//创建环形链表,要几个节点?返回的节点为环尾,即josephu.next = 环首
public Josephu getJosephu(int n){
//初始化头
Josephu josephu = new Josephu(0);
//记录环首
Josephu Next = null;
int length = n;
int i = 1;
while (length>0){
josephu.next = new Josephu(i);
if(i == 1){
Next = josephu.next;
}
i++;
length --;
josephu = josephu.next;
}
josephu.next = Next;
return josephu;
}
//打印约瑟夫环算法 n: 多少人 m: 第几个出列
public void GetJosepho(int n , int m){
//完成合理性校验,即参数正确,环不为空
if(n<1 || m<1){
System.out.println("无效参数");
return;
}
//得到约瑟夫环及所有数据
Josephu josephu = getJosephu(n);
int num = 1;
while (n - num > -1){
int sum = m;
while (true){
if(sum==1){
System.out.println("第" + num +"个出列的为:"+josephu.next);
josephu.next = josephu.next.next;
break;
}
sum--;
josephu = josephu.next;
}
num++;
}
}
}
//节点
class Josephu{
public int id;
public Josephu next;
public Josephu(int id){
this.id = id;
}
@Override
public String toString() {
return "Josephu{" +
"id=" + id +
'}';
}
}
-
老师的方式:创建思路一致,出圈方式都是数到一个m删一个
-
原链表管理器上加一个遍历链表的方法
//遍历环
public void showJosepho(int n){
if(n<1){
System.out.println("无效参数");
return;
}
//得到约瑟夫环及所有数据
Josephu josephu = getJosephu(n);
//尽量不要改变公用参数 n
int i = n;
while (i-->0){
System.out.println(josephu.next);
josephu = josephu.next;
}
}
1.4 栈
1.4.1 数组模拟栈
- stack – 先入后出的有序列表
- 限制只能在线性表的同一端进行的特殊线性表,任何操作只能在栈顶操作,栈底固定
- 子程序调用:跳往子程序时将主程序的下一步的地址存到栈中,子程序执行完毕,从栈中取出地址,继续主程序
- push:入 pop:出
数组模拟栈
import java.util.Scanner;
public class domTow01 {
//测试数组模拟的栈
public static void main(String[] args) {
Stack stack = new Stack(2);
//用循环菜单
String key = "";
boolean loop = true;//控制退出菜单
Scanner scanner = new Scanner(System.in);
while (loop){
System.out.println("show:显示栈");
System.out.println("push:添加数据");
System.out.println("pop:取出数据");
System.out.println("exit:退出程序");
System.out.println("num:栈内数据个数");
//接收数据
key = scanner.next();
switch (key){
case "show":
stack.list();
break;
case "push":
System.out.println("请输入:");
int value = scanner.nextInt();
try {
stack.push(value);
} catch (Exception e) {
//提示错误信息就行,不关闭进程
System.out.println(e.getMessage());
}
break;
case "pop":
stack.pop();
break;
case "exit":
//退出一定要释放资源!
scanner.close();
loop = false;
break;
case "num":
System.out.println(stack.num());
break;
default:
System.out.println("what?????????");
break;
}
}
}
}
//定义栈结构类
class Stack{
private int maxSize;
private int arr[];
//栈顶
private int top = -1;
//构造器
public Stack(int maxSize){
this.maxSize = maxSize;
arr = new int[maxSize];
}
//判断栈满
public boolean isFull(){
return top != maxSize - 1 ?false:true;
}
//判断栈空
public boolean isNoone(){
return top == -1?true:false;
}
//入栈
public void push(int value){
if(isFull()){
System.out.println("栈满");
}
top ++;
arr[top] = value;
}
//出栈
public int pop(){
if(isNoone()){
//这里必须有返回值,不能return 0,用异常处理的方式
throw new RuntimeException("栈空");
}
top -- ;
return arr[top+1];
}
//遍历栈--从栈顶显示数据
public void list(){
if (isNoone()){
System.out.println("栈空");
}
//遍历不能改变原始结构-即top位置
for (int i = top;i>-1;i--){
System.out.println(arr[i]);
}
}
//栈内数据个数
public int num(){
return top+1;
}
}
- 基础巩固:一定要注意数组对象的创建,异常处理的使用方式,中断进程or不中断进程打印异常信息。
1.4.2 链表模拟栈
- 这个比数组好写,只用链表最基础简单的结构就能实现
public class domTow02 {
public static void main(String[] args) {
Stack2 stack1 = new Stack2(1);
Stack2 stack2 = new Stack2(2);
Stack2 stack3 = new Stack2(3);
Stack2 stack4 = new Stack2(4);
//测试,出入遍历随便写
// SingleLinkedList5 singleLinkedList5 = new SingleLinkedList5(2);
// singleLinkedList5.push(singleLinkedList5.getHead(), stack1);
// singleLinkedList5.list(singleLinkedList5.getHead());
// System.out.println("成功取出:"+singleLinkedList5.pop(singleLinkedList5.getHead()));
// singleLinkedList5.push(singleLinkedList5.getHead(), stack2);
// singleLinkedList5.push(singleLinkedList5.getHead(), stack3);
// singleLinkedList5.push(singleLinkedList5.getHead(), stack4);
// singleLinkedList5.list(singleLinkedList5.getHead());
// System.out.println("成功取出:"+singleLinkedList5.pop(singleLinkedList5.getHead()));
// System.out.println("成功取出:"+singleLinkedList5.pop(singleLinkedList5.getHead()));
// System.out.println("成功取出:"+singleLinkedList5.pop(singleLinkedList5.getHead()));
}
}
//链表对象类
class Stack2{
int id;
Stack2 next;
public Stack2(int id){
this.id = id;
}
@Override
public String toString() {
return "Stack2{" +
"id=" + id +
'}';
}
}
//链表管理器类
class SingleLinkedList5{
Stack2 head = new Stack2(0);
int maxSize;
int nowSize=0;
//栈最大容量
public SingleLinkedList5(int maxSize){
this.maxSize = maxSize;
}
//获取头节点
public Stack2 getHead(){
return head;
}
//添加---模拟栈只能是尾插
public void push(Stack2 head,Stack2 stack){
if(nowSize == maxSize){
System.out.println("栈满");
return;
}
Stack2 temp = head;
while (true){
if (temp.next == null){
temp.next = stack;
break;
}
temp = temp.next;
}
nowSize ++;
}
//取出---同样只能尾插
public Stack2 pop(Stack2 head){
if(head.next == null && nowSize == 0){
System.out.println("栈空");
return null;
}
Stack2 temp = head;
while (true){
if(temp.next.next==null){
Stack2 cur = temp.next;
temp.next = null;
nowSize --;
return cur;
}
temp = temp.next;
}
}
//遍历
public void list(Stack2 head){
if(head.next == null && nowSize == 0){
System.out.println("栈空");
return;
}
Stack2 temp = head.next;
while (true){
if (temp == null){
break;
}
System.out.println(temp);
temp = temp.next;
}
}
}
1.4.3 栈实现综合计算器
-
树栈:存放表达式
-
符号栈:存放运算符
-
整数型跟char型可以混用!!!符号底层也是一个数字来表示
算法思路
- 通过index(索引),遍历表达式
- 数字—直接入树栈
- 符号—入符号栈
- 当前符号栈为空,直接入栈
- 符号栈有操作符,进行比较
- 当前操作符优先级小于等于栈中操作符,需要从树栈中pop出两个数,从符号栈中pop一个符号,进行运算,计算结果入树栈,当前符号入符号栈
- 当前操作符的优先级大于栈中的操作符,直接入符号栈
- 表达式扫描完毕,顺序从树栈和符号栈中pop出相应的数字和符号,运算
- 最后树栈中只有一个数字,符号栈为空,就是表达式的结果
- 代码实现
//栈模拟简单计算器代码实现
public class domTow04 {
public static void main(String[] args) {
//创建树栈和符号栈
Stack3 numStack = new Stack3(10);
Stack3 operStack = new Stack3(10);
//测试的表达式
String str = "1+2*3-1";
//需要的变量
int num1 = 0;
int num2 = 0;
int oper = 0; //符号 : char型和整型可以混用
int ver = 0; //计算结果
int index = 0; //扫描的索引
char ch = ' ';//当前扫描到的数据
//扫描遍历
while (true) {
//扫描依次得到表达式每一个字符
ch = str.substring(index , index+1).charAt(0);//记住字符串的substring方法
//如果是符号
if (numStack.isOper(ch)) {
//判断符号栈是否为空
if (operStack.isNoone()) {
operStack.push(ch);
} else {
//判断符号优先级
//当前优先级小于等于栈中符号的优先级
if (operStack.priority(ch) <= operStack.priority(operStack.watch())) {
num1 = numStack.pop();
num2 = numStack.pop();
oper = operStack.pop();
ver = numStack.cal(num1, num2, oper);
numStack.push(ver);
//算完一定要将当前的运算符放到符号栈
operStack.push(ch);
} else {
//当前优先级大于栈中符号的优先级
operStack.push(ch);
}
}
} else {
//不是符号则直接入数栈
//第一次入栈时一定要注意! 遍历出来的是字符,表达的数字与真实有差异,想要字符转数字,ch - 48 或 ch - '0'
numStack.push(ch-'0');
}
index++;
//遍历结束
if (index == str.length()) {
break;
}
}
//对扫描后的数栈跟符号栈顺序遍历计算
while (true) {
//若符号栈为空,则不需要计算
if (operStack.isNoone()) {
ver = numStack.pop();
break;
}
//顺序计算
num1 = numStack.pop();
num2 = numStack.pop();
oper = operStack.pop();
ver = numStack.cal(num1, num2, oper);
numStack.push(ver);
}
//输出计算结果
System.out.println("计算:" + str + " = " + ver);
}
}
//先创建一个栈
class Stack3{
private int maxSize;
private int arr[];
//栈顶
private int top = -1;
//构造器
public Stack3(int maxSize){
this.maxSize = maxSize;
arr = new int[maxSize];
}
//判断栈满
public boolean isFull(){
return top != maxSize - 1 ?false:true;
}
//判断栈空
public boolean isNoone(){
return top == -1?true:false;
}
//入栈
public void push(int value){
if(isFull()){
System.out.println("栈满");
}
top ++;
arr[top] = value;
}
//出栈
public int pop(){
if(isNoone()){
//这里必须有返回值,不能return 0,用异常处理的方式
throw new RuntimeException("栈空");
}
top -- ;
return arr[top+1];
}
//遍历栈--从栈顶显示数据
public void list(){
if (isNoone()){
System.out.println("栈空");
}
//遍历不能改变原始结构-即top位置
for (int i = top;i>-1;i--){
System.out.println(arr[i]);
}
}
//栈内数据个数
public int num(){
return top+1;
}
//在原来的数组栈中扩展功能
//返回运算符优先级---这是程序员定的
//运算符优先级由数字表示
public int priority(int oper){
if(oper == '+' || oper == '-'){
return 0;
}else if(oper == '*' || oper == '/'){
return 1;
}else {
//假定只有加减乘除功能
return -1;
}
}
//判断是不是运算符
public boolean isOper(char val){
return val == '+' || val == '-' || val == '*' || val == '/' ;
}
//计算方法---注意计算顺序
public int cal(int num1 , int num2 , int oper){
int ver = 0;
switch (oper){
case '+':
ver = num1 + num2;
break;
case '-':
ver = num2 - num1;
break;
case '*':
ver = num1 * num2;
break;
case '/':
ver = num2 / num1;
break;
default:
break;
}
return ver;
}
//查看栈顶数据,不是pop
public int watch(){
return arr[top];
}
}
- 以上代码只能实现一位数的运算,不能实现多位数运算
- 改进后的代码如下—只改扫描遍历那块即可
String keepNum = ""; //定义字符串变量,拼接字符串
//扫描遍历
while (true) {
//扫描依次得到表达式每一个字符
ch = str.substring(index , index+1).charAt(0);//记住字符串的substring方法
//如果是符号
if (numStack.isOper(ch)) {
//判断符号栈是否为空
if (operStack.isNoone()) {
operStack.push(ch);
} else {
//判断符号优先级
//当前优先级小于等于栈中符号的优先级
if (operStack.priority(ch) <= operStack.priority(operStack.watch())) {
num1 = numStack.pop();
num2 = numStack.pop();
oper = operStack.pop();
ver = numStack.cal(num1, num2, oper);
numStack.push(ver);
//算完一定要将当前的运算符放到符号栈
operStack.push(ch);
} else {
//当前优先级大于栈中符号的优先级
operStack.push(ch);
}
}
} else {
//不是符号则直接入数栈
//第一次入栈时一定要注意! 遍历出来的是字符,表达的数字与真实有差异,想要字符转数字,ch - 48 或 ch - '0'
//numStack.push(ch-'0');
//不能发现是一个数字直接入数栈,可能是多位数
//定义字符串变量keepNum,拼接字符串
//遍历当前数字的后一位,是数字则继续遍历,符号入栈
keepNum += ch;
if (index == str.length() - 1) {
//如果遍历到最后一位,直接入栈,避免index+2的指针溢出问题
numStack.push(Integer.parseInt(keepNum));
} else {
if (numStack.isOper(str.substring(index + 1, index + 2).charAt(0))) {
//下一位是字符,将字符串直接入栈---强转
numStack.push(Integer.parseInt(keepNum));
//入栈后keepNum置空
keepNum = "";
} else {
}
}
}
index++;
//遍历结束
if (index == str.length()) {
break;
}
}
1.5 前缀,中缀,后缀表达式
1.5.1 简介
- 前缀表达式(波兰表达式): 运算符位于数字之前
-
- 从右向左扫描表达式 (3+4) * 5 -6 ---- - * + 3 4 5 6
- 中缀表达式: 常见的运算表达式: (3+4) * 5 -6
-
- 对于人简洁明了:对于计算机较为困难
- 后缀表达式(逆波兰表达式): 运算符位于数字之后
-
- 对计算机来说最方便的表达式
- 从左到右依次扫描,遇到符号就计算,不用考虑优先级
- (3+4) * 5 -6 ---- 3 4 + 5 * 6 -
- a + b ---- a b +
- a + (b - c) — a b c - +
- a + (b - c) * d — a b c - d * +
- a + d * (b - c) ---- a d b c - * +
- a = b + c — a b c + =
1.5.2 逆波兰计算器
- 输入后缀表达式,使用栈实现
- 支持小括号及多位整数
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
public class domTow05 {
public static void main(String[] args) {
//定义逆波兰表达式
//为方便数字和符号中使用空格隔开
String expression = "3 4 + 5 * 6 -";
List<String> list = getListString(expression);
System.out.println("list = " + list);
System.out.println(cal(list));
}
//将表达式依次放入ArrayList
public static List<String> getListString(String expression){
//分割表达式,返回一个String数组
String[] split = expression.split(" ");
List<String> list = new ArrayList<String>();
for (String s : split) {
//将split数组里面的数据依次放入list
list.add(s);
}
return list;
}
//运算式
public static int cal(List<String> list){
//只需要一个栈就行
Stack<String> stack = new Stack<String>();
//遍历list
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("错误符号");
}
//数字转字符串放到栈中
stack.push(res + "");
}
}
//时刻要注意数据形式转换
return Integer.parseInt(stack.pop());
}
}
- 涉及到的基础回顾加强:
-
- ArrayList
- split 分割字符串: 将字符串变为字符串数组
- 正则表达式
- 格式转换: 数字转字符串 num + “” , 字符串转数字 Integer.parseInt()
1.5.3 中缀转后缀
- 后缀表达式计算机理解方便,人不容易写出来,需要在开发中将中缀表达式自动转换为后缀表达式
步骤分析:
初始化两个栈,中间栈(中间结果),符号栈
顺序扫描表达式
遇到操作数,入中间栈
遇到运算符,比较优先级
4.1 符号栈为空或左括号,直接入符号栈
4.2 当前的优先级比栈顶高也入符号栈
4.3 否则将符号栈栈顶运算符弹出并压入中间栈,当前运算符与符号栈栈顶运算符继续比较
遇到括号时:
5.1 左括号直接入符号栈
5.2 右括号:依次弹出栈顶运算符,压入中间栈,直到遇到右括号为止,将右括号丢弃
遍历到表达式完
将符号栈运算符全部依次放入中间栈
依次弹出中间栈元素,转为字符串并反转
//将中缀表达式转为对应的list
public static List<String> toInfix(String s){
//定义list,存放中缀表达式
List<String> ls = new ArrayList<String>();
int i = 0; // 指针:遍历字符串s
String str ; //拼接多位数
char c ; //每遍历一个字符,放入list
do {
//遍历到的非数字,直接加入到ls
if((c = s.charAt(i)) < 48 || (c = s.charAt(i)) > 57){
ls.add("" + c);
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;
}
//中缀转后缀表达式
public static String parseSuffix(List<String> ls){
//初始化两个栈
Stack<String> milStack = new Stack<String>();
Stack<String> operStack = new Stack<String>();
//由于中间栈全程只是添加,用list更方便
List<String> mil = new ArrayList<String>();
//利用for取出list里面的数据
for (String item : ls) {
if(item.matches("\\d+")){//正则表达式匹配多位数
//是数字直接入栈
milStack.push(item);
}else if(item.equals("(")){
operStack.push(item);
}else if(item.equals(")")){
// 右括号:依次弹出栈顶运算符,压入中间栈,直到遇到右括号为止,将右括号丢弃
String ch;
while (!(ch = operStack.pop()).equals("(")){
milStack.push(ch);
}
}else {
//最后就是运算符了,这里需要比较优先级
//如果为空,直接入栈
if(operStack.size() == 0){
operStack.push(item);
}else if(priority(item) > priority(watch(operStack))){//当前的优先级比栈顶高也入符号栈
operStack.push(item);
}else {
//否则将符号栈栈顶运算符弹出并压入中间栈,当前运算符与符号栈栈顶运算符继续比较
while (true){
milStack.push(operStack.pop());
if(operStack.size() == 0){
operStack.push(item);
break;
}
if(priority(item) > priority(watch(operStack))){
operStack.push(item);
break;
}
}
}
}
}
//将符号栈运算符全部依次放入中间栈
while (operStack.size() != 0){
milStack.push(operStack.pop());
}
//要返回的字符串
String str = "";
//将中间栈的数据以字符串形式返回
while (milStack.size() != 0){
//拼接时候注意一下,省的再反转
str = milStack.pop() + " " + str;
}
//删除最后一个空格
str = str.substring(0 , str.length() - 1);
return str;
}
//判断优先级
public static int priority(String oper){
if(oper.equals("+")|| oper.equals("-")){
return 0;
}else if(oper.equals("*") || oper.equals("/")){
return 1;
}else {
//假定只有加减乘除功能
return -1;
}
}
//查看栈顶数据
public static String watch(Stack<String> st){
String str = st.pop();
st.push(str);
return str;
}
- main方法测试
//测试将中缀表达式转为list
String str = "1+((2+3)*4)-5";
List<String> infix = toInfix(str);
System.out.println(infix);
//测试转换后的表达式
System.out.println(parseSuffix(infix));
- 需要注意到的点:
-
- 注意两个栈同时使用时进出栈别出错
- 栈判空用它自己的size()方法
- 在之后会有反转字符串加空格等操作时,在拼接的时候就可以拼接为想要的
1.5.4 综合逆波兰计算器
- 将1.5.2 与1.5.3 里面的算法合并,就是完整的逆波兰计算器
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
public class domTow05 {
public static void main(String[] args) {
//测试将中缀表达式转为list
String str = "1+((2+3)*4)-5";
List<String> infix = toInfix(str);
System.out.println(infix);
//测试转换后的表达式
System.out.println(parseSuffix(infix));
//综合测试
System.out.println(cal(getListString(parseSuffix(infix))));
}
//将表达式依次放入ArrayList
public static List<String> getListString(String expression){
//分割表达式,返回一个String数组
String[] split = expression.split(" ");
List<String> list = new ArrayList<String>();
for (String s : split) {
//将split数组里面的数据依次放入list
list.add(s);
}
return list;
}
//运算式
public static int cal(List<String> list){
//只需要一个栈就行
Stack<String> stack = new Stack<String>();
//遍历list
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("错误符号");
}
//数字转字符串放到栈中
stack.push(res + "");
}
}
//时刻要注意数据形式转换
return Integer.parseInt(stack.pop());
}
//将中缀表达式转为对应的list
public static List<String> toInfix(String s){
//定义list,存放中缀表达式
List<String> ls = new ArrayList<String>();
int i = 0; // 指针:遍历字符串s
String str ; //拼接多位数
char c ; //每遍历一个字符,放入list
do {
//遍历到的非数字,直接加入到ls
if((c = s.charAt(i)) < 48 || (c = s.charAt(i)) > 57){
ls.add("" + c);
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;
}
//中缀转后缀表达式
public static String parseSuffix(List<String> ls){
//初始化两个栈
Stack<String> milStack = new Stack<String>();
Stack<String> operStack = new Stack<String>();
//由于中间栈全程只是添加,用list更方便
List<String> mil = new ArrayList<String>();
//利用for取出list里面的数据
for (String item : ls) {
if(item.matches("\\d+")){//正则表达式匹配多位数
//是数字直接入栈
milStack.push(item);
}else if(item.equals("(")){
operStack.push(item);
}else if(item.equals(")")){
// 右括号:依次弹出栈顶运算符,压入中间栈,直到遇到右括号为止,将右括号丢弃
String ch;
while (!(ch = operStack.pop()).equals("(")){
milStack.push(ch);
}
}else {
//最后就是运算符了,这里需要比较优先级
//如果为空,直接入栈
if(operStack.size() == 0){
operStack.push(item);
}else if(priority(item) > priority(watch(operStack))){//当前的优先级比栈顶高也入符号栈
operStack.push(item);
}else {
//否则将符号栈栈顶运算符弹出并压入中间栈,当前运算符与符号栈栈顶运算符继续比较
while (true){
milStack.push(operStack.pop());
if(operStack.size() == 0){
operStack.push(item);
break;
}
if(priority(item) > priority(watch(operStack))){
operStack.push(item);
break;
}
}
}
}
}
//将符号栈运算符全部依次放入中间栈
while (operStack.size() != 0){
milStack.push(operStack.pop());
}
//要返回的字符串
String str = "";
//将中间栈的数据以字符串形式返回
while (milStack.size() != 0){
//拼接时候注意一下,省的再反转
str = milStack.pop() + " " + str;
}
//删除最后一个空格
str = str.substring(0 , str.length() - 1);
return str;
}
//判断优先级
public static int priority(String oper){
if(oper.equals("+")|| oper.equals("-")){
return 0;
}else if(oper.equals("*") || oper.equals("/")){
return 1;
}else {
//假定只有加减乘除功能
return -1;
}
}
//查看栈顶数据
public static String watch(Stack<String> st){
String str = st.pop();
st.push(str);
return str;
}
}
1.6 递归
1.6.1 简介
-
递归调用机制:自己调用自己
-
调用一次开一个栈空间,每个空间独立
-
空间复杂度!!!
-
递归问题列举
public class domTow06 {
public static void main(String[] args) {
System.out.println(factorial(4));
}
//打印问题
public static void test(int n){
if(n >2){
test(n - 1);
}
System.out.println(n);
}
//阶乘问题
public static int factorial(int n){
if(n == 1){
return 1;
}else {
return factorial(n-1) * n;
}
}
}
1.6.2 递归能解决的问题和规则
- 谷歌算法大赛 *
- 算法中:快排,归并等算法问题
- 迷宫问题,汉诺塔
规则
- 执行一个方法,创建一个新的受保护的独立空间(栈空间)
- 方法调用即在栈顶再压入一个栈空间,执行时候遵循栈的原则
- 方法的局部变量不能互相影响,如果方法中使用的是引用类型变量(数组),则共享该引用类型的数据
- 递归必须向退出递归的方向逼近,否则会无限循环,到栈满至栈溢出
- 一个方法执行完毕,遇到return,谁调用返回给谁,同时当方法执行完毕或return时,该方法也执行完毕
1.6.3 迷宫问题
- 实际可选路程为六行五列的迷宫,中间设置障碍
public class domTow07 {
public static void main(String[] args) {
//创建二维数组,模拟迷宫
int[][] map = new int[8][7];
//迷宫周边设置为墙
//上下置为一
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][3] = 1;
// map[2][3] = 1;
// map[3][3] = 1;
setWay(map , 1 , 1);
showMap(map);
}
//地图情况
public static void showMap(int[][] map){
for (int[] ints : map) {
for (int anInt : ints) {
System.out.print(anInt + " ");
}
System.out.println();
}
}
/**
* map:地图
* i:横轴
* j:纵轴
* boolean:判断找没找到
* 出口 map[6][5]
* 当地图上map[i][j] = 0:此路没有经过
* 当地图上map[i][j] = 1:墙
* 当地图上map[i][j] = 2:通路,可以走
* 当地图上map[i][j] = 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 { //如果该点不等于0 , map可能为1,2,3
//代表起点位置开始就走不通,返回false,该迷宫无解
return false;
}
}
}
}
1.6.4 迷宫最短路径问题
- 小球的路径与程序员设计的路径有关
- 当前算法调优只能限制找路方法
- 上下左右的排序依次测试打印数组中2的个数最少的为最短路径
//修改找路方法 上右下左
public static boolean setWay2(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 { //如果该点不等于0 , map可能为1,2,3
//代表起点位置开始就走不通,返回false,该迷宫无解
return false;
}
}
}
1.6.5 八皇后问题
- 问题描述:8*8的棋盘,放置八枚皇后棋子,要求每一行,每一列,每一条斜线只能有一枚棋子
- 用一维数组就能实现----不画棋盘!!!
public class domTow08 {
//定义共有多少个皇后
int max = 8;
//定义数组保存皇后被放置的位置--一维数组就可以
int[] array = new int[max];
//统计共有多少种方法
int num = 0;
public static void main(String[] args) {
//测试八皇后算法
new domTow08().check(0);
}
//放置第n个皇后
private void check(int n){
//当需要放置第九个皇后时,意味着八个皇后已经放置妥当
if(n == max){
num ++;
System.out.print("第 " + num +" 种方法:");
print();
return;
}
/**
* 核心的for循环!!!!!!!!!!!
* 每次递归都有一个for循环,一定会执行完
* 因为是for循环一定会执行完的原因,完成回溯的算法思想,能将所有可能性执行一遍
* */
//依次放入皇后,判断冲突
for (int i = 0 ; i < max ; i ++){
//先把当前皇后放到该行的第1列
array[n] = i;
//判断第n个皇后放到第i列是否冲突
if(judge(n)){ //不冲突
//接着放
check(n+1);
}
}
}
//放置第n个皇后时,检测该皇后是否与之前的冲突,若冲突则为false
private boolean judge(int n) {
for(int i = 0 ; i < n ; i ++){
//是否在同一列,或同一斜线(即二维棋盘上横轴差等于纵轴差)
//判断同一行没必要
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 = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
System.out.println();
}
}
1.7 哈希表
1.7.1 简介
- key–value 结构 :根据关键字码值直接进行访问的数据结构
- 通过关键码值映射到表中一个位置来访问记录,该映射关系函数为散列函数,存放记录的数组称为散列表(哈希表)
- 缓存层
- 数组+链表 — 链表数组
- 数组+二叉树 — 二叉树数组
1.7.2 hash经典笔试题
-
某公司,有新员工报道时,要求该员工的信息加入(id,姓名,性别,年龄,住址…)输入该员工id时,查到该员工所有信息。
要求:不用数据库,速度快 => hash(散列)
import java.util.Scanner;
public class domThree03 {
public static void main(String[] args) {
//创建Hash表
HashTable hashTable = new HashTable(7);
//写个菜单
String key = "";
Scanner scanner = new Scanner(System.in);
while (true){
System.out.println("add: 添加");
System.out.println("list: 显示");
System.out.println("exit: 退出");
System.out.println("find: 查找");
key = scanner.next();
switch (key){
case "add":
System.out.println("输入id");
int id = scanner.nextInt();
System.out.println("输入姓名");
String name = scanner.next();
hashTable.add(new Emp(id,name));
break;
case "list":
hashTable.list();
break;
case "exit":
scanner.close();
System.exit(0);
case "find":
System.out.println("输入id");
int id2 = scanner.nextInt();
hashTable.listByid(id2);
break;
default:
System.out.println("好好输");
break;
}
}
}
}
//链表对象类--员工信息类
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 +
'}';
}
}
//创建链表管理器
class EmpLinkedList{
//链表的head,直接指向Emp
public Emp head;
//添加--默认最后
//id自分配,自增
public void add(Emp emp){
//如果是添加第一个雇员
if (head == null){
head = emp;
return;
}
Emp cur = head;
while (true){
if (cur.next == null){
break;
}
cur = cur.next;
}
cur.next = emp;
}
//遍历链表
public void list(int no){
if(head == null){
System.out.println("第 "+ no +"条链表为空");
return;
}
Emp cur = head;
while (true){
System.out.println("第"+ no +"条链表" + cur);
if(cur.next == null){
break;
}
cur = cur.next;
}
}
//根据id查找雇员,找到返回Emp,没找到返回null
public Emp listById(int id){
if(head == null){
System.out.println("链表为空");
return null;
}
Emp cur = head;
while (true){
if (cur.id == id){
return cur;
}
if(cur.next == null){
return null;
}
cur = cur.next;
}
}
}
//hash表
class HashTable{
//用管理器类创建叔祖
private EmpLinkedList[] empLinkedListsArray;
public int size;
//构造器
public HashTable(int size){
this.size = size;
//初始化
empLinkedListsArray = new EmpLinkedList[size];
//不能直接向空数组添加
for (int i = 0 ; i < size ; i ++){
empLinkedListsArray[i] = new EmpLinkedList();
}
}
//添加雇员
public void add(Emp emp){
//根据id,得到该员工数据应该添加到那条链表
int empLinkedListsNo = hashFun(emp.id);
//将emp添加到对应的链表
empLinkedListsArray[empLinkedListsNo].add(emp);
}
//编写散列函数
public int hashFun(int id){
return id % size;
}
//遍历哈希表
public void list(){
for (int i = 0; i < size ; i ++){
empLinkedListsArray[i].list(i);
}
}
//根据id查找
public void listByid(int id){
int empLinkedListsNo = hashFun(id);
Emp emp = empLinkedListsArray[empLinkedListsNo].listById(id);
if(emp != null){
System.out.println(emp);
}else {
System.out.println("没找着");
}
}
}
- 总的来说,就是在原来链表基础上,给链表管理器再套一层hashTable类—链表数组
1.8 树
1.8.1 存储结构
数组存储
-
根据下表访问,访问速度快
-
缺点:插入值需要整体移动,效率低
-
数组扩容时需要创建新的数组,将原来数据拷贝到新数组
-
arrList集合(object类型数组)底层自适应扩容同样也是这个原理,但他是按照比例扩容
链表存储
-
添加非常方便,避免了数组的扩容问题
-
缺点:每次遍历都需要从头节点开始
树存储
- 优化存储,读取的速度,保证插入,修改,删除的速度
- 如果使用二叉排序树存储数据,对数据的增删改查都将提高
1.8.2 二叉树遍历
前序,中序,后续—父节点输出顺序来区分
- 前序,根节点—左节点—右节点
- 中序,左节点—头节点—右节点
- 后续,左节点—右节点—头节点
//二叉树
public class domThree04 {
public static void main(String[] args) {
BinaryTree binaryTree = new BinaryTree();
//创建节点
Node n1 = new Node(1, "老王");
Node n2 = new Node(2, "老张");
Node n3 = new Node(3, "老刘");
Node n4 = new Node(4, "老吴");
Node n5 = new Node(5, "老孙");
//暂且手动创建 n1为根节点
n1.setLeft(n2);
n1.setRight(n3);
n2.setLeft(n4);
n2.setRight(n5);
//把根节点给到位
binaryTree.setRoot(n1);
System.out.println("前序");
binaryTree.preOrder();
System.out.println("中序");
binaryTree.infixOrder();
System.out.println("后序");
binaryTree.postOrder();
}
}
//创建树
class BinaryTree {
private Node root;
public void setRoot(Node root) {
this.root = root;
}
//前序遍历
public void preOrder() {
if(this.root != null){
this.root.preOrder();
}else {
System.out.println("二叉树为空");
}
}
//中序
public void infixOrder() {
if (this.root != null) {
this.root.infixOrder();
} else {
System.out.println("二叉树为空");
}
}
//后续
public void postOrder(){
if (this.root != null) {
this.root.postOrder();
} else {
System.out.println("二叉树为空");
}
}
}
//创建节点
class Node {
private int id;
private String name;
private Node left;
private Node right;
//构造器
public Node(int id , String name){
this.id = id;
this.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 infixOrder(){
//递归向左
if(this.left != null){
this.left.infixOrder();
}
//输出父节点
System.out.println(this);
if(this.right != null){
this.right.infixOrder();
}
}
//后续
public void postOrder(){
if(this.left != null) {
this.left.postOrder();
}
if(this.right != null){
this.right.postOrder();
}
System.out.println(this);
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public Node getLeft() {
return left;
}
public void setLeft(Node left) {
this.left = left;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Node getRight() {
return right;
}
public void setRight(Node right) {
this.right = right;
}
@Override
public String toString() {
return "Node{" +
"id=" + id +
", name='" + name +
'}';
}
}
1.8.3 二叉树查找
-
前序中序后序查找
-
main函数
//测试查找
System.out.println("前序");
binaryTree.preSeek(3);
System.out.println("中序");
binaryTree.infixSeek(3);
System.out.println("后序");
binaryTree.postSeek(3);
- 节点类
//查找方法
//前序
public Node preseek(int id) {
System.out.println("进入前序遍历");//统计查询递归查找次数。下同
if(this.id == id){
return this;
}
Node falg = null;
if(this.left != null){
falg = this.left.preseek(id);
}
if(falg != null){
return falg;
}
if(this.right != null){
falg = this.right.preseek(id);
}
return falg;
}
//中序
public Node infixSeek(int id) {
Node falg = null;
if(this.left != null){
falg = this.left.infixSeek(id);
}
if(falg != null){
return falg;
}
System.out.println("进入中序遍历");
if(this.id == id){
return this;
}
if(this.right != null){
falg = this.right.infixSeek(id);
}
return falg;
}
//后序
public Node postSeek(int id) {
Node falg = null;
if(this.left != null) {
falg = this.left.postSeek(id);
}
if(falg != null){
return falg;
}
if(this.right != null){
falg = this.right.postSeek(id);
}
if(falg != null){
return falg;
}
System.out.println("进入后续遍历");
if (this.id == id){
return this;
}
return falg;
}
- 树类
//查找方法
//前序
public void preSeek(int id) {
if (this.root != null) {
System.out.println(this.root.preseek(id));
} else {
System.out.println("二叉树为空");
}
}
//中序
public void infixSeek(int id) {
if (this.root != null) {
System.out.println(this.root.infixSeek(id));
} else {
System.out.println("二叉树为空");
}
}
//后序
public void postSeek(int id) {
if (this.root != null) {
System.out.println(this.root.postSeek(id));
} else {
System.out.println("二叉树为空");
}
- 一定要搞明白递归思想,否则这里会绕
1.8.4 删除节点
-
这里规定:
-
如果是叶子节点,直接删除
-
如果不是叶子节点,删除子树
-
二叉树是单向的,参考链表删除节点思想
-
树类
//删除
public void delet(int id) {
if (this.root != null) {
if(root.getId() == id){
root = null ;
System.out.println("删除成功");
}else {
root.delet(id);
}
} else {
System.out.println("二叉树为空");
}
}
- 节点类
//删除节点(前序)
public void delet(int id){
if(this.left != null && this.left.id == id){
this.left = null;
return;
}
if (this.right != null && this.right.id == id){
this.right = null;
return;
}
if(this.left != null){
this.left.delet(id);
}
if(this.right != null){
this.right.delet(id);
}
}
1.8.5 顺序存储二叉树
- 数组和树能互相转换
- 必须满足是满二叉树 (完全二叉树) 的情况
public static void main(String[] args) {
int[] arr = {1 , 2 , 3 ,4 , 6 , 7};
ArrBinaryTree arrBinaryTree = new ArrBinaryTree(arr);
arrBinaryTree.preOrder();
}
}
class ArrBinaryTree {
private int[] arr;
public ArrBinaryTree(int arr[]){
this.arr = arr;
}
//重载遍历方法,使在主函数中更简洁
public void preOrder(){
preOrder(0);
}
//实现对数组以前序二叉树方式遍历
public void preOrder(int index) {
if(this.arr == null && arr.length < index){
System.out.println("无法遍历");
}
//打印当前遍历位置的数据
//前序,中序,后序只需要改变这句话与左右递归位置即可
System.out.println(arr[index]);
//主要核心在于左子节点下标为2n+1,右子节点为2n+2
//左递归
if(2*index + 1 < arr.length) {
preOrder(2*index + 1);
}
//右递归
if(2*index +2 < arr.length) {
preOrder(2*index + 2);
}
}
}
1.8.6 线索化二叉树
- 充分利用叶子节点的空指针
- 叶子节点的左右指针分别指向前驱与后继节点
public class domThree08 {
public static void main(String[] args) {
Hn h1 = new Hn(1, "li");
Hn h2 = new Hn(2, "wu");
Hn h3 = new Hn(3, "su");
Hn h4 = new Hn(4, "lu");
Hn h5 = new Hn(5, "gu");
BinaryTree2 binaryTree2 = new BinaryTree2();
binaryTree2.setRoot(h1);
h1.setLeft(h2);
h1.setRight(h3);
h2.setLeft(h4);
h2.setRight(h5);
binaryTree2.threadHd();
//测试线索化后叶子节点的左右子树是否指向前驱/后继节点
System.out.println(h4.getRight());
}
}
//节点
class Hn {
public int id ;
public String name;
private Hn left;
private Hn right;
/**
* 标记该节点是否为叶子节点,在遍历时需要该参数
* leftType: 0 ->左子树 1 -> 前驱节点
* rightType: 0 ->右子树 1 -> 后继节点
* */
private int leftType;
private int rightType;
public Hn(int id , String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "Hn{" +
"id=" + id +
", name='" + name +
'}';
}
public Hn getLeft() {
return left;
}
public void setRight(Hn right) {
this.right = right;
}
public Hn getRight() {
return right;
}
public void setLeft(Hn left) {
this.left = left;
}
public int getLeftType() {
return leftType;
}
public void setLeftType(int leftType) {
this.leftType = leftType;
}
public int getRightType() {
return rightType;
}
public void setRightType(int rightType) {
this.rightType = rightType;
}
}
//树
class BinaryTree2 {
private Hn root;
private Hn pre = null; // 指向当前节点的前驱节点
public void setRoot(Hn root) {
this.root = root;
}
//重载线索化方法
public void threadHd(){
threadHn(root);
}
//线索化二叉树(中序)
public void threadHn(Hn node) {
if(node == null) {
return;
}
//线索化左子树
threadHn(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;
//右子树
threadHn(node.getRight());
}
}
1.8.7 遍历线索化二叉树
- 遍历顺序应当和线索化的顺序保持一致
- 线索化后不需要递归就能实现
//线索化中序遍历
public void preOrder() {
Hn 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();
}
}
1.8.8 赫夫曼树
- 最优二叉树
- WPL : 树的带权路径长度
- 最优二叉树:WPL最小的二叉树
import java.util.ArrayList;
import java.util.Collections;
public class domThree09 {
public static void main(String[] args) {
int arr[] = {1 , 4 , 6 , 2 , 3} ;
preOrder(HuffmanTree(arr));
}
//创建赫夫曼树的方法
public static Node3 HuffmanTree(int[] arr) {
//为了操作方便,遍历arr数组
//将每个元素构成一个Node
//将Node全部放入Arrlist
ArrayList<Node3> node3s = new ArrayList<>();
for (int i : arr) {
node3s.add(new Node3(i));
}
while (node3s.size() > 1) {
//排序--从小到大
Collections.sort(node3s);
//取出权值最小的两个二叉树
Node3 left = node3s.get(0);
Node3 right = node3s.get(1);
//构建新二叉树
Node3 parent = new Node3(left.val + right.val);
parent.left = left;
parent.right = right;
//挂在树上后删除List里的left与right
node3s.remove(left);
node3s.remove(right);
//将新建树根节点放入List
node3s.add(parent);
}
//返回赫夫曼树root节点
return node3s.get(0);
}
//遍历方法
public static void preOrder(Node3 root) {
if(root != null) {
root.preOrder();
}else {
System.out.println("空树");
}
}
}
//创建节点
class Node3 implements Comparable<Node3> { //让node实现Comparable接口
int val;
Node3 left;
Node3 right;
public Node3(int val) {
this.val = val;
}
//前序遍历
public void preOrder() {
System.out.println(this);
if(this.left != null){
this.left.preOrder();
}
if(this.right != null) {
this.right.preOrder();
}
}
@Override
public String toString() {
return "Node3{" +
"val=" + val +
'}';
}
@Override
public int compareTo(Node3 o) {
//表示从小到大排序
return this.val - o.val;
}
}
1.8.9 数据压缩
1.8.9.1 创建赫夫曼树
1.8.9.2 生成赫夫曼编码表
1.8.9.3 赫夫曼编码字节数组
1.8.9.4 字节转二进制字符串
1.8.9.5 赫夫曼解码
1.8.9.6 赫夫曼压缩文件
1.8.9.7 赫夫曼解压文件
1.8.10 二叉排序树
2.算法
2.1 排序算法
2.1.1 简介
- 排序算法的介绍
- 将一组数据,依照指定的顺序排序
分类:
- 内部排序:所有数据加载到内部存储器中进行排序(重点)
-
- 插入排序
-
- 直接插入
- 希尔排序
- 选择排序
-
- 简单选择排序
- 堆排序
- 交换排序
-
- 冒泡
- 快排
- 归并排序
- 基数排序(桶排序)
- 外部排序:数据量过大,需要加载外部存储进行排序
2.1.2 时间复杂度
度量程序优越与否
- 事后统计:运行一下看效果(涉及因素过多,不推荐)
- 事前估算:时间复杂度评估
时间频度
-
T(n) :算法中语句的执行次数
-
忽略常数项
-
忽略低次项
-
忽略系数
-
T(n)简写为:O(f(n)) —时间复杂度
-
对数阶:时间复杂度O(log2 n)比线性n还小,非常优秀的算法,仅次于常熟阶O(1)
while (i < n){
i = i * 2;
}
平均时间复杂度与最坏时间复杂度
- 平均:所有可能性等概率出现运行时间
- 最坏:最坏情况运行的时间,算法时间上限!
- 稳定性:
-
- 冒泡,交换,选择,插入:n值小时较好
- 基数,希尔
- 快排,归并,堆:n值大时较好
空间复杂度
- 算法中所耗费的存储空间
- 更看重时间(用户希望越快越好)—缓存:空间换时间
2.1.3 冒泡排序
- 理解为从右往左排
public class domThree01 {
public static void main(String[] args) {
int arr[] = {3 , 4 , 1 , 2 , 6};
effervescent(arr);
System.out.println(Arrays.toString(arr));
}
//冒泡排序方法
public static void effervescent(int[] arr){
int temp;//用来交换
for(int i = 0 ; i < arr.length ; i ++){
for(int j = 1 ; j < arr.length - i ; j ++){
if(arr[j] < arr[j - 1]){
temp = arr[j];
arr[j] = arr[j - 1];
arr[j - 1] = temp;
}
}
}
}
}
- 算法调优:如果某一趟第二次的遍历没有发生交换,说明此时的数组已经有序
//冒泡排序方法
public static void effervescent(int[] arr){
int temp;//用来交换
for(int i = 0 ; i < arr.length ; i ++){
boolean flag = true;
for(int j = 1 ; j < arr.length - 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));
if(flag){
break;
}
}
}
2.1.4 选择排序
- 理解为从左往右排
- 最多只交换 n-1 次,相较于冒泡排序,交换的次数有质的飞跃,在大量随机数的考验下,比冒泡排序快好多倍
//选择排序
public static void choose(int[] arr){
for (int i = 0 ; i < arr.length-1 ; i ++){
int temp = arr[i];
int index = i; //记录下标
for (int j = i + 1 ; j < arr.length ; j ++){
if(arr[i] > arr[j]){
temp = arr[j];
index = j;
}
}
if(index != i){ //优化,最小值没有变化就不用交换
arr[index] = arr[i];
arr[i] = temp;
}
}
}
2.1.5 插入排序
-
从左往右排
-
从第二个位置开始循环向前遍历找到插入位置
//插入排序
public static void insert(int[] arr){
for(int i = 1 ; i < arr.length ; i ++){
//待插入数
int val = arr[i];
//待插入数前面的下标
int index = i - 1;
// 待插入数比前一个数大或到了下标为0的位置,退出循环
while (index >= 0 && val < arr[index]){
arr[index + 1] = arr[index];
index --;
}
//退出while循环即找到插入位置
if(i != index + 1){ // 这里优化可有可无,性能不会发生明显变化
arr[index + 1] = val;
}
}
}
2.1.6 希尔排序
-
插入排序缺点:如果最小的几个数都在数组的最后面,位移次数太多,影响效率
-
改良版插入排序: 分组思想,逐步变为接近有序的数组
-
缩小增量排序
//希尔排序
public static void shier(int[] arr){
//定义交换的第三方变量
int temp = 0;
//分组,定义步长
for(int gap = arr.length / 2 ; gap > 0 ; gap /= 2){
//按照分组遍历
for (int i = gap ; i < arr.length ; i ++ ){
//遍历组中所有元素
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;
}
}
}
}
}
- 以上遍历每组所有数据中采用交换法改变数据位置,算法稍复杂,性能跟冒泡差不多(慢!)
- 以上并没有用到插入排序思想,而真正的希尔排序是在插入排序的思想基础上升级优化的
- 用移动法优化—性能高于插入排序!!!百倍!!!
//改良版希尔排序
public static void shier2(int[] arr){
//确定增量
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];
while (j - gap >= 0 && temp < arr[j - gap]){
arr[j] = arr[j - gap];
j -= gap;
}
//退出while循环,找到插入位置
arr[j] = temp;
}
}
}
2.1.7 快速排序
-
对冒泡排序进行改进
-
随便找一个值开始就行,这里以中间值为例
-
比希尔快一点: 空间换时间典型算法
//快速排序
public static void quick(int[] arr , int left , int right){
int l = left;
int r = right;
int pivot = arr[(left + right) / 2];
int temp; // 交换的临时变量
while (l < r){
//左遍历
while (arr[l] < pivot){
l ++;
}
//右变量
while (arr[r] > pivot){
r --;
}
//左右遍历完毕
if(l >= r){
break;
}
//交换
temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
//交换完以后,若当前位置与中间量相等,会一直卡在这里,手动移动,避免死循环
if(arr[l] == pivot){
r --;
}
if(arr[r] == pivot){
l ++;
}
}
//从这里开始为递归做准备
if(l == r){
l += 1;
r -= 1;
}
//向左递归
if(left < r){ //只剩一个数据时,不会进递归
quick(arr , left , r);
}
//向右递归
if(right > r){
quick(arr , l , right);
}
}
2.1.8 归并排序
-
先分后和(重点在和!)
-
n个数据需要合并n-1次,即调用最复杂的合并方法只需要n-1次
-
800 0000 条数据仅需要2s , 非常快!!!
-
该方法需要开一个临时存储空间,即new一个等长数组
public class domThree01 {
public static void main(String[] args) {
int arr[] = {1 , 2 , 6 , 4 , -1};
//临时等长空间
int[] temp = new int[arr.length];
//测试合并方法
merge(arr , 0 , 2 , 4 , temp);
//测试完整归并方法
mergeSore(arr , 0 , arr.length - 1 , temp);
System.out.println(Arrays.toString(arr));
}
//归并排序
//合并方法
public static void merge(int arr[] , int left , int mid , int right , int temp[]){
int i = left; //左边初始
int j = mid + 1; //右边初始
int t = 0; //第三方数组temp初始
//左右两边数据按照规则放到temp数组,直到一边全部放完
while (i <= mid && j <= right){
if(arr[i] > arr[j]){
temp[t] = arr[j];
t ++;
j ++;
}else {
temp[t] = arr[i];
t ++;
i ++;
}
}
//剩余部分按照顺序全部放到temp数组
if(i == mid + 1){
while (j <= right){
temp[t] = arr[j];
t ++;
j ++;
}
}else {
while (i <= mid){
temp[t] = arr[i];
t ++;
i ++;
}
}
//将temp数组拷贝到原始数组
int tempLeft = left;
for (int n = 0; n < t; n++) {
arr[tempLeft] = temp[n];
tempLeft ++ ;
}
}
//分+和
public static void mergeSore(int[] arr, int left , int right , int[] temp){
if(left < right){
int mid = (left + right) / 2 ;
//向左分解
mergeSore(arr , left , mid , temp);
//向右分解
mergeSore(arr , mid + 1 , right , temp);
//合并
merge(arr , left , mid , right , temp);
}
}
2.1.9 基数排序
-
桶排序的扩展
-
稳定性排序
-
按照位数进行分类排序
-
连递归都用不到!!!
-
速度比归并还要快得多
-
缺点就是占用内存:需要额外开辟十倍数据空间
//基数排序
public static void radix(int[] arr){
//找到最大数
int max = arr[0];
for (int i : arr) {
if(max < i){
max = i;
}
}
//计算数字位数巧妙方法,变成字符串,用它自带的length方法
int maxLength = (max + "").length();
//定义桶
int[][] temp = new int[10][arr.length];
//定义数组记录每个桶中数组元素个数
int[] sum = new int[10];
for (int cal = 0 , n = 1; cal < maxLength ; cal ++ , n *= 10){
//便利原数组,按规则放到桶中
for (int i : arr) {
//取出元素各位
int val = i / n % 10 ;
//放入桶中
temp[val][sum[val]] = i;
sum[val]++;
}
//便利每一个桶,放到原数组
int index = 0 ;
for (int k = 0 ; k < sum.length ; k ++) {
//桶里有数据才放入原数组
if (sum[k] != 0){
//循环放入
for (int j = 0; j < sum[k] ; j ++){
arr[index] = temp[k][j];
index ++ ;
}
//放完置空
sum[k] = 0;
}
}
}
}
2.1.10 堆排序
-
树结构的应用
-
O(nlogn)
-
速度与归并相差不多,但不用开空间,整体性能较好
//堆排序
public static void heap(int[] arr) {
int temp;
for (int i = arr.length / 2 - 1 ; i >= 0 ; i --) {
//将待排序序列构造成一个大顶堆,这时整个序列最大值为根节点
adjust(arr , i , arr.length);
}
for (int j = arr.length - 1 ; j > 0 ; j --) {
//根节点与末尾元素交换,末尾元素为最大值
temp = arr[j];
arr[j] = arr[0];
arr[0] = temp;
//循环遍历其余n-1个元素
adjust(arr , 0 , j);
}
}
//将i为非叶子节点的数组调整为大顶堆,---只处理该节点和该节点以下的数据
public static void adjust(int arr[] , int i , int length) { //数组 非叶子节点的索引 对多少个元素进行调整
int temp = arr[i];
for (int k = 2*i + 1 ; k < length ; k = i *2 +1) {
if(k + 1 < length && arr[k] < arr[k + 1]) {
k ++; //左右节点取大值
}
if(arr[k] > temp) {
arr[i] = arr[k];
i = k; //循环遍历子节点
}else {
break;
}
} // for循环结束,该非叶子节点为该模块的最大值
arr[i] = temp;
}
2.2 查找算法
2.2.1 线性查找
- 遍历数组,这个简单
//线性查找,顺序比对
public static int seq(int[] arr , int n){
for (int i = 0 ; i < arr.length ; i ++){
if(arr[i] == n){
return i;
}
}
throw new RuntimeException("未找到");
}
2.1.2 二分查找
- 也叫折半查找,要求数据是有序的
- 这个简单
//二分查找
public static int binary(int arr[] , int n , int left , int right){
if(left > right){
throw new RuntimeException("未找到");
}
//中间值
int mid = (left + right) / 2 ;
if(arr[left] == n){
return left ;
}else if(arr[mid] == n){
return mid;
}else if(arr[right] == n){
return right;
}else {
if(arr[mid] < n){
//右边
return binary(arr , n , mid + 1 , right - 1);
}else {
//左边
return binary(arr , n , left + 1 , mid - 1);
}
}
}
- 主方法调用时注意异常处理
int arr[] = {1, 3 , 4 , 5 , 6 , 6 , 8};
try {
System.out.println(binary(arr , 9 , 0 , arr.length - 1));
} catch (Exception e) {
System.out.println(e.getMessage());
}
- 当需要找到多个数据时
//二分查找
public static ArrayList binary(int arr[] , int n , int left , int right){
if(left > right){
throw new RuntimeException("未找到");
}
//中间值
int mid = (left + right) / 2 ;
if(arr[left] == n ){
ArrayList<Integer> list = new ArrayList<Integer>();
while (arr[left] == n && left <= right){
list.add(left);
left ++;
}
return list ;
}else if(arr[mid] == n){
ArrayList<Integer> list = new ArrayList<Integer>();
int mid2 = mid;
while (arr[mid2] == n && mid2 <= right){
list.add(mid2);
mid2 ++;
}
while (arr[mid - 1] == n && mid > left){
list.add(mid - 1);
mid --;
}
return list ;
}else if(arr[right] == n){
ArrayList<Integer> list = new ArrayList<Integer>();
while (arr[right] == n && left < right){
list.add(right);
right--;
}
return list ;
}else {
if(arr[mid] < n){
//右边
return binary(arr , n , mid + 1 , right - 1);
}else {
//左边
return binary(arr , n , left + 1 , mid - 1);
}
}
}
2.1.3 插值查找
- 在二分查找的基础上,将原来的mid中间值修改为按照比例定义中间值位置
- 将mid取值改为:
int mid = left + (right - left) * (n - arr[left]) / (arr[right] - arr[left]);
//二分查找
public static ArrayList binary(int arr[] , int n , int left , int right){
if(left > right){
throw new RuntimeException("未找到");
}
//中间值
int mid = left + (right - left) * (n - arr[left]) / (arr[right] - arr[left]);
if(arr[left] == n ){
ArrayList<Integer> list = new ArrayList<Integer>();
while (arr[left] == n && left <= right){
list.add(left);
left ++;
}
return list ;
}else if(arr[mid] == n){
ArrayList<Integer> list = new ArrayList<Integer>();
int mid2 = mid;
while (arr[mid2] == n && mid2 <= right){
list.add(mid2);
mid2 ++;
}
while (arr[mid - 1] == n && mid > left){
list.add(mid - 1);
mid --;
}
return list ;
}else if(arr[right] == n){
ArrayList<Integer> list = new ArrayList<Integer>();
while (arr[right] == n && left < right){
list.add(right);
right--;
}
return list ;
}else {
if(arr[mid] < n){
//右边
return binary(arr , n , mid + 1 , right - 1);
}else {
//左边
return binary(arr , n , left + 1 , mid - 1);
}
}
}
- 插值查找,适合关键字分布均匀的数据,否则还是用二分查找稳定一点