前言:
对尚硅谷韩顺平老师所讲的数据结构和算法做了一个较完整的笔记。
包含:文字,图片,原代码。
个人看法:数据结构讲的比较通俗易懂,算法有一定难度不太建议初学者观看。
数据结构和算法
数据结构和算法的概述
数据结构和和算法的关系
- 数据(date)j结构(structure)是一门研究组织数据方式的学科,有了编程语言也就有了数据结构,学好数据结构可 以编写出更加漂亮,更加有效的代码。
- 要学好数据结构就要多多考虑如何将生活中遇到的问题,用程序去解决。
- 程序 = 数据结构 +算法
- 数据结构是算法的基础,换言之,想要学好算法,需要把数据结构学到位。
数据结构
线性结构和非线性结构
- 线性结构作为最常用的数据结构,其特点是数据元素之间存在一对一的线性关系
- 线性结构有两种不同的存储结构,即顺序存储结构(数组)和链式存储结构(链表)。顺序存储的线性表称为顺序 表,顺序表中的存储元素是连续的
- 链式存储的线性表称为链表,链表中的存储元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息
- 线性结构常见的有:数组、队列、链表和栈…
非线性结构
非线性结构包括:二维数组,多维数组,广义表,树结构,图结构
稀疏 sparsearray 数组
基本介绍:
当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组。
稀疏数组的处理方法是:
- 第一行记录数组一共有几行几列,有多少个不同的值
- 把具有不同值的元素的行列及值记录在一个小规模的数组中,从而缩小程序的规模
应用实例:棋盘
- 使用稀疏数组,来保留类似前面的二维数组(棋盘、地图等等)
- 把稀疏数组存盘,并且可以从新恢复原来的二维数组数
- 整体思路分析
二维数组 转 稀疏数组的思路
- 遍历原始的二维数组,得到有效数据的个数 sum
- 根据sum 就可以创建稀疏数组 sparseArr int[sum + 1] [3]
- 将二维数组的有效数据数据存入到稀疏数组
稀疏数组转原始的二维数组的思路
- 先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组,比如上面的 chessArr2 = int [11] [11]
- 在读取稀疏数组后几行的数据,并赋给 原始的二维数组 即可.
代码实现
package hhhh;
import java.sql.Date;
public class SparseArray {
public static void main(String[] args) {
//创建一个原始的二维数组 11*11
//0:表示没有棋子, 1 表示 黑子 2 表示 蓝子
int chessArr1[][] = new int[11][11];
chessArr1[3][4]=1;
chessArr1[5][3]=2;
chessArr1[6][3]=2;
//输出原始的二维数组
System.out.println("原始的二维数组~~");
for(int[] row : chessArr1) {
for (int data : row) {
System.out.printf("%d\t",data);
}
System.out.println();
}
//将二维数组 转 稀疏数组的思路
//1.先遍历二维数组 得到非0数据的个数
int sum = 0;
for (int i = 0; i < chessArr1.length; i++) {
for (int j = 0; j < chessArr1.length; j++) {
if(chessArr1[i][j]!=0) {
sum++;
}
}
}
//2.创建对应的稀疏数组
int sparseArr[][] = new int[sum+1][3];
//给稀疏数组赋值
sparseArr[0][0]=11;
sparseArr[0][1]=11;
sparseArr[0][2]=sum;
//遍历二维数组,将非零的值存放到spareArr 中
int count = 0;//count 用于记录是第几个非0数据
for (int i = 0; i < 11; i++) {
for (int j = 0; j < 11; j++) {
if (chessArr1[i][j]!=0) {
count++;
sparseArr[count][0] = i;
sparseArr[count][1] = j;
sparseArr[count][2] = chessArr1[i][j];
}
}
}
//输出稀疏数组的形式
System.out.println();
System.out.println("得到稀疏数组为~~");
for (int i = 0; i < sparseArr.length; i++) {
System.out.printf("%d\t%d\t%d\t\n",sparseArr[i][0],sparseArr[i][1],sparseArr[i][2]);
}
System.out.println();
//将稀疏数组恢复成原始的二维数组
//1.先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组
int chessArr2[][] = new int[sparseArr[0][0]][sparseArr[0][1]];
//2.在读取稀疏数组后几行的数据(从第二行开始),并赋值给原始的二维数组即可
for (int i = 1; i < sparseArr.length; i++) {
chessArr2[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][2];
}
System.out.println();
System.out.println("恢复后的二维数组");
for (int[] row : chessArr2) {
for (int date : row) {
System.out.printf("%d\t",date);
}
System.out.println();
}
}
}
队列
- 队列是一个有序列表,可以用数组或是链表来实现。
- 遵循先入先出的原则。即:先存入队列的数据,要先取出。后存入的要后取出
- 示意图:(使用数组模拟队列示意图)
数组模拟队列思路
- 队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如下图, 其中 maxSize 是该队列的最大容量。
- 因为队列的输出、输入是分别从前后端来处理,因此需要两个变量 front 及 rear 分别记录队列前后端的下标, front 会随着数据输出而改变,而 rear 则是随着数据输入而改变,如图所示
- 当我们将数据存入队列时称为”addQueue”,addQueue 的处理需要有两个步骤:思路分析
- 将尾指针往后移:rear+1 , 当 front == rear 【空】
- 若尾指针 rear 小于队列的最大下标 maxSize-1,则将数据存入 rear 所指的数组元素中,否则无法存入数据。 rear == maxSize - 1[队列满]
- rear是队列最后【含】,front是队列的最前元素【不含】
代码实现:数组模拟队列思路
import java.util.Scanner;
public class zezhen11 {
public static void main(String[] args) {
//创建一个队列
ArrayQueue arrayQueue = new ArrayQueue(6);
char key = ' ';//接口用户输入
Scanner s = new Scanner(System.in);
boolean loop = true;
//输出菜单
while (loop){
System.out.println("s:显示队列");
System.out.println("e:退出程序");
System.out.println("a:添加数据到队列");
System.out.println("g:从队列取出数据");
System.out.println("h:查看队列头部数据");
key = s.next().charAt(0);//接受一个字符
switch (key){
case 's':
arrayQueue.showQueue();
break;
case 'a':
System.out.println("请输入一个数");
int value = s.nextInt();
arrayQueue.addQueue(value);
break;
case 'g':
try {
int res = arrayQueue.gerQueue();
System.out.println("去除的数据为:"+res);
}catch(Exception e){
System.out.println(e.getMessage());//捕抓异常信息
}
break;
case 'h':
try {
int res = arrayQueue.headQueue();
System.out.println("队列头数据为:"+res);
}catch (Exception e){
System.out.println(e.getMessage());
}
break;
case 'e':
s.close();
loop = false;
break;
default:
break;
}
}
}
}
//使用数组模拟队列
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("队列满不能加入数据");
return;
}
rear++;
arr[rear] = n;
}
//出队列
public int gerQueue() {
//判断队列是否为空
if (isEmpty()) {
//通过抛出异常来处理
throw new RuntimeException("队列空,不能取出");
}
front++;
return arr[front];
}
//显示队列
public void showQueue() {
//遍历
if (isEmpty()) {
System.out.println("队列为空");
}
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i] + "");
}
}
//显示队列的头数据
public int headQueue() {
//判断
if (isEmpty()) {
throw new RuntimeException("队列空");
} else {
return arr[front + 1];
}
}
}
数组模拟环形队列
对前面的数组模拟队列的优化,充分利用数组。因此将数组看做是一个环形。(通过取模的方式来实现即可)
- 思路如下
- front变量的含义做一个调整;front就指向队列的第一个元素,也就是说arr[front]就是队列的第一个元素 front的初始值=0
- rear 变量的含义做一个调整;rear就指向队列的最后一个元素的后一个位置,因为希望空出一个空间数为约定,rear的初始值=0
- 当对列满时,条件是(rear+1)%maxSize=front[满]
- 对队列为空的条件,rear ==front[空]
- 当我们这样分析,队列中有效的数据的个数**(rear+maxSize-front)%maxSize**// rear=1 front=0
- 我们就可以在原来的队列上修改得到,一个环形队列
代码实现:数组模拟环形队列
package project;
import java.util.Scanner;
public class zezhen9 {
public static void main(String[] args) {
// 创建一个队列
CircleArray arrayQueue = new CircleArray(4);
char key = ' ';// 接口用户输入
Scanner s = new Scanner(System.in);
boolean loop = true;
// 输出菜单
while (loop) {
System.out.println("s:显示队列");
System.out.println("e:退出程序");
System.out.println("a:添加数据到队列");
System.out.println("g:从队列取出数据");
System.out.println("h:查看队列头部数据");
key = s.next().charAt(0);// 接受一个字符
switch (key) {
case 's':
arrayQueue.showQueue();
break;
case 'a':
System.out.println("请输入一个数");
int value = s.nextInt();
arrayQueue.addQueue(value);
break;
case 'g':
try {
int res = arrayQueue.gerQueue();
System.out.println("去除的数据为:" + res);
} catch (Exception e) {
System.out.println(e.getMessage());// 捕抓异常信息
}
break;
case 'h':
try {
int res = arrayQueue.headQueue();
System.out.println("队列头数据为:" + res);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'e':
s.close();
loop = false;
break;
default:
break;
}
}
}
}
// 使用数组模拟队列
class CircleArray {
private int maxSize;// 表达数组的最大容量
private int front;
// front变量的含义做一个调整;front就指向队列的第一个元素,也就是说arr[front]就是队列的第一个元素
// front的初始值=0
private int rear;
// rear 变量的含义做一个调整;rear就指向队列的最后一个元素的后一个位置,因为希望空出一个空间数为约定
// rear的初始值=0
private int[] arr;// 用于存放数据
// 创建队列构造器
public CircleArray(int arrmaxSize) {
maxSize = arrmaxSize;
arr = new int[maxSize];
front = 0;
rear = 0;
}
// 判断队列是否满
public boolean isFull() {
return (rear + 1) % maxSize == front;
}
// 判断队列是否为空
public boolean isEmpty() {
return rear == front;
}
// 添加数据到队列
public void addQueue(int n) {
// 判断队列是否满
if (isFull()) {
System.out.println("队列满不能加入数据");
return;
}
// 直接将数据加入
arr[rear] = n;
// 将rear后移,这里必须考虑取模
rear = (rear + 1) % maxSize;
}
// 出队列
public int gerQueue() {
// 判断队列是否为空
if (isEmpty()) {
// 通过抛出异常来处理
throw new RuntimeException("队列空,不能取出");
}
// 这里需要分析出front是指向队列的第一个元素
// 1.先把front对应的值保留到一个临时变量
// 2.将front后移,考虑取模
// 3.将临时保存的变量返回
int value = arr[front];
front = (front + 1) % maxSize;
return value;
}
// 显示队列
public void showQueue() {
// 遍历
if (isEmpty()) {
System.out.println("队列为空");
}
// 思路:从front开始遍历,遍历多少个元素
// 动脑筋
for (int i = front; i < front + size(); i++) {
System.out.printf("arr[%d]=%d\n", i % maxSize, arr[i % maxSize]);
}
}
// 求当前队列有效数据的个数
public int size() {
return (rear + maxSize - front) % maxSize;
}
// 显示队列的头数据
public int headQueue() {
// 判断
if (isEmpty()) {
throw new RuntimeException("队列空");
} else {
return arr[front];
}
}
}
链表(Linked List)介绍
-
链表是有序的列表,但是它在内存中是存储如下
-
小结
- 链表是以节点的方式来存储,是链式存储
- 每个节点包含 data 域, next 域:指向下一个节点
- 如图:发现链表的各个节点不一定是连续存储
- 链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定
-
单链表(带头结点) 逻辑结构示意图如下
单链表的应用:
第一步:在添加英雄时,直接添加到链表的尾部
代码实现
public class zezhen99 {
public static void main(String[] args){
//先创建节点
HeroNode heroNode1 = new HeroNode(1,"","");
HeroNode heroNode2 = new HeroNode(2,"","");
HeroNode heroNode3 = new HeroNode(3,"","");
//创建一个链表
SingleLikedlist singleLikedlist = new SingleLikedlist();
singleLikedlist.add(heroNode1);
singleLikedlist.add(heroNode2);
singleLikedlist.add(heroNode3);
singleLikedlist.showList();
}
}
//定义singleLinkedlist 管理我们的英雄
class SingleLikedlist{
//先初始化一个头节点
private HeroNode head = new HeroNode(0,"","");
//添加节点到单项节点
//当不考虑编号的顺序时,找到当前链表的最后节点,将最后这个节点next指向新节点
public void add(HeroNode heroNode){
//因为head节点不能动,因此我们需要一个辅助变量
HeroNode temp =head;
while (true){
if(temp.next == null){
break;
}
//没有到最后
temp = temp.next;
}
//当退出while循环,temp指向链表最后
//将最后这个节点的next指向新的节点
temp.next = heroNode;//将最后一个节点的next指向新加入的点
}
//显示链表
public void showList(){
//先判断链表是否为空
if(head.next == null){
return;
}
HeroNode temp = head.next;
while(true){
//是否到链表最后
if(temp == null){
break;
}
//输出节点信息
System.out.println(temp);
temp = temp.next;
}
}
}
//定义一个HeroNode,每一个HeroNode就是一个节点
class HeroNode{
//定义节点的私有属性 编号 姓名 外号 和next
public int no;
public String name;
public String nickname;
public HeroNode next;
//构造器
public HeroNode(){
}
public HeroNode(int no,String name, String nickname){
this.no = no;
this.name = name;
this.nickname = nickname;
}
//重写toString 方便显示
public String toString(){
return "Heronode [no= "+no+",name="+name+",nickname="+nickname+"]";
}
}
第二步:根据排名插入到指定位置
如果有这个排名,则添加失败,并给出提示
代码实现
public class zezhen99 {
public static void main(String[] args){
//先创建节点
HeroNode heroNode1 = new HeroNode(1,"","");
HeroNode heroNode2 = new HeroNode(2,"","");
HeroNode heroNode3 = new HeroNode(3,"","");
//创建一个链表
SingleLikedlist singleLikedlist = new SingleLikedlist();
singleLikedlist.addByOeder(heroNode3);
singleLikedlist.addByOeder(heroNode1);
singleLikedlist.addByOeder(heroNode2);
singleLikedlist.showList();
}
}
//定义singleLinkedlist 管理我们的英雄
class SingleLikedlist{
//先初始化一个头节点
private HeroNode head = new HeroNode(0,"","");
//添加节点到单项节点
//当不考虑编号的顺序时,找到当前链表的最后节点,将最后这个节点next指向新节点
public void add(HeroNode heroNode){
//因为head节点不能动,因此我们需要一个辅助变量
HeroNode temp =head;
while (true){
if(temp.next == null){
break;
}
//没有到最后
temp = temp.next;
}
//当退出while循环,temp指向链表最后
//将最后这个节点的next指向新的节点
temp.next = heroNode;//将最后一个节点的next指向新加入的点
}
//第二种方式在添加英雄时,根据排名将英雄插入到指定位置
//(如果有这个排名,则添加失败,并给出提示)
public void addByOeder(HeroNode heroNode){
//因为头节点不能动,因此我们仍然通过一个辅助指针(变量)来帮助找到添加的位置
//因为单链表,因为我们找的temp是位于添加位置的前一个节点,否则插入不了
HeroNode temp = head;
boolean flag = 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;//说明编号存在
}
temp = temp.next;//后移,遍历当前链表
}
//判断flag的值
if(flag){
System.out.printf("准备插入的英雄的编号%d 已经存在了,不能加入\n",heroNode.no);
}else {
//插入到链表中,temp的后面
heroNode.next = temp.next;
temp.next = heroNode;
}
}
//显示链表
public void showList(){
//先判断链表是否为空
if(head.next == null){
return;
}
HeroNode temp = head.next;
while(true){
//是否到链表最后
if(temp == null){
break;
}
//输出节点信息
System.out.println(temp);
temp = temp.next;
}
}
}
//定义一个HeroNode,每一个HeroNode就是一个节点
class HeroNode{
//定义节点的私有属性 编号 姓名 外号 和next
public int no;
public String name;
public String nickname;
public HeroNode next;
//构造器
public HeroNode(){
}
public HeroNode(int no,String name, String nickname){
this.no = no;
this.name = name;
this.nickname = nickname;
}
//重写toString 方便显示
public String toString(){
return "Heronode [no= "+no+",name="+name+",nickname="+nickname+"]";
}
}
第三步:修改节点
思路
- 先找到该节点,通过遍历
- temp.name = newHeroNode.name;temp.nickname = newHeroNode.nickname
代码实现
public class zezhen99 {
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(3,"zezhen","");
//创建一个链表
SingleLikedlist singleLikedlist = new SingleLikedlist();
singleLikedlist.addByOeder(heroNode3);
singleLikedlist.addByOeder(heroNode1);
singleLikedlist.addByOeder(heroNode2);
singleLikedlist.update(heroNode4);
singleLikedlist.showList();
}
}
//定义singleLinkedlist 管理我们的英雄
class SingleLikedlist{
//先初始化一个头节点
private HeroNode head = new HeroNode(0,"","");
//添加节点到单项节点
//当不考虑编号的顺序时,找到当前链表的最后节点,将最后这个节点next指向新节点
public void add(HeroNode heroNode){
//因为head节点不能动,因此我们需要一个辅助变量
HeroNode temp =head;
while (true){
if(temp.next == null){
break;
}
//没有到最后
temp = temp.next;
}
//当退出while循环,temp指向链表最后
//将最后这个节点的next指向新的节点
temp.next = heroNode;//将最后一个节点的next指向新加入的点
}
//第二种方式在添加英雄时,根据排名将英雄插入到指定位置
//(如果有这个排名,则添加失败,并给出提示)
public void addByOeder(HeroNode heroNode){
//因为头节点不能动,因此我们仍然通过一个辅助指针(变量)来帮助找到添加的位置
//因为单链表,因为我们找的temp是位于添加位置的前一个节点,否则插入不了
HeroNode temp = head;
boolean flag = 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;//说明编号存在
}
temp = temp.next;//后移,遍历当前链表
}
//判断flag的值
if(flag){
System.out.printf("准备插入的英雄的编号%d 已经存在了,不能加入\n",heroNode.no);
}else {
//插入到链表中,temp的后面
heroNode.next = temp.next;
temp.next = heroNode;
}
}
//修改节点的信息,根据no编号来修改,即no编号不能改
//说明
//1.根据 newHeroNode 的no来修改即可
public void update(HeroNode newheroNode){
//判断是否为空
if(head.next == null){
System.out.println("链表为空~");
return;
}
//找到需要修改的节点,根据no编号
//定义一个辅助变量
HeroNode temp = head.next;
boolean flag = false;//表示是否找到该节点
while (true){
if(temp == null){
break;//已经遍历完链表
}
if(temp.no == newheroNode.no){
//找到
flag = true;
break;
}
temp = temp.next;
}
//根据flag 判断是否找到修改的节点
if(flag){
temp.name = newheroNode.name;
temp.nickname = newheroNode.nickname;
}else{//没有找到
System.out.printf("没有找到编号%d的节点,不能修改\n",newheroNode.no);
}
}
//显示链表[遍历]
public void showList(){
//先判断链表是否为空
if(head.next == null){
return;
}
HeroNode temp = head.next;
while(true){
//是否到链表最后
if(temp == null){
break;
}
//输出节点信息
System.out.println(temp);
temp = temp.next;
}
}
}
//定义一个HeroNode,每一个HeroNode就是一个节点
class HeroNode{
//定义节点的私有属性 编号 姓名 外号 和next
public int no;
public String name;
public String nickname;
public HeroNode next;
//构造器
public HeroNode(){
}
public HeroNode(int no,String name, String nickname){
this.no = no;
this.name = name;
this.nickname = nickname;
}
//重写toString 方便显示
public String toString(){
return "Heronode [no= "+no+",name="+name+",nickname="+nickname+"]";
}
}
第四步:删除节点
代码实现
public class zezhen99 {
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(3,"zezhen","");
//创建一个链表
SingleLikedlist singleLikedlist = new SingleLikedlist();
singleLikedlist.addByOeder(heroNode3);
singleLikedlist.addByOeder(heroNode1);
singleLikedlist.addByOeder(heroNode2);
singleLikedlist.update(heroNode4);
singleLikedlist.del(2);
singleLikedlist.showList();
}
}
//定义singleLinkedlist 管理我们的英雄
class SingleLikedlist{
//先初始化一个头节点
private HeroNode head = new HeroNode(0,"","");
//添加节点到单项节点
//当不考虑编号的顺序时,找到当前链表的最后节点,将最后这个节点next指向新节点
public void add(HeroNode heroNode){
//因为head节点不能动,因此我们需要一个辅助变量
HeroNode temp =head;
while (true){
if(temp.next == null){
break;
}
//没有到最后
temp = temp.next;
}
//当退出while循环,temp指向链表最后
//将最后这个节点的next指向新的节点
temp.next = heroNode;//将最后一个节点的next指向新加入的点
}
//第二种方式在添加英雄时,根据排名将英雄插入到指定位置
//(如果有这个排名,则添加失败,并给出提示)
public void addByOeder(HeroNode heroNode){
//因为头节点不能动,因此我们仍然通过一个辅助指针(变量)来帮助找到添加的位置
//因为单链表,因为我们找的temp是位于添加位置的前一个节点,否则插入不了
HeroNode temp = head;
boolean flag = 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;//说明编号存在
}
temp = temp.next;//后移,遍历当前链表
}
//判断flag的值
if(flag){
System.out.printf("准备插入的英雄的编号%d 已经存在了,不能加入\n",heroNode.no);
}else {
//插入到链表中,temp的后面
heroNode.next = temp.next;
temp.next = heroNode;
}
}
//修改节点的信息,根据no编号来修改,即no编号不能改
//说明
//1.根据 newHeroNode 的no来修改即可
public void update(HeroNode newheroNode){
//判断是否为空
if(head.next == null){
System.out.println("链表为空~");
return;
}
//找到需要修改的节点,根据no编号
//定义一个辅助变量
HeroNode temp = head.next;
boolean flag = false;//表示是否找到该节点
while (true){
if(temp == null){
break;//已经遍历完链表
}
if(temp.no == newheroNode.no){
//找到
flag = true;
break;
}
temp = temp.next;
}
//根据flag 判断是否找到修改的节点
if(flag){
temp.name = newheroNode.name;
temp.nickname = newheroNode.nickname;
}else{//没有找到
System.out.printf("没有找到编号%d的节点,不能修改\n",newheroNode.no);
}
}
//删除节点
//思路
//1. head 不能动,因此我们需要一个temp辅助节点找到待删除的前一个节点
//2. 说明我们在比较时,是temp.next.no 和 需要删除的节点的no比较
public void del(int no){
HeroNode temp = head;
boolean flag = false;//标志是否找到待删除节点的
while(true){
if (temp.next == null){//已经到了链表的最后
break;
}
if (temp.next.no == no){
//找到的待删除节点的前一个节点temp
flag = true;
break;
}
temp = temp.next;//temp后移,遍历
}
//判断flag
if (flag){//找到
//可以删除
temp.next = temp.next.next;
}else{
System.out.printf("要删除的%d 节点不存在\n",no);
}
}
//显示链表[遍历]
public void showList(){
//先判断链表是否为空
if(head.next == null){
return;
}
HeroNode temp = head.next;
while(true){
//是否到链表最后
if(temp == null){
break;
}
//输出节点信息
System.out.println(temp);
temp = temp.next;
}
}
}
//定义一个HeroNode,每一个HeroNode就是一个节点
class HeroNode{
//定义节点的私有属性 编号 姓名 外号 和next
public int no;
public String name;
public String nickname;
public HeroNode next;
//构造器
public HeroNode(){
}
public HeroNode(int no,String name, String nickname){
this.no = no;
this.name = name;
this.nickname = nickname;
}
//重写toString 方便显示
public String toString(){
return "Heronode [no= "+no+",name="+name+",nickname="+nickname+"]";
}
}
单链表面试题(新浪、百度、腾讯…)
-
求单链表中有效节点个数
-
public class zezhen99 { 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(3,"zezhen",""); //创建一个链表 SingleLikedlist singleLikedlist = new SingleLikedlist(); singleLikedlist.addByOeder(heroNode3); singleLikedlist.addByOeder(heroNode1); singleLikedlist.addByOeder(heroNode2); singleLikedlist.update(heroNode4); singleLikedlist.del(2); singleLikedlist.showList(); System.out.println("有效的节点个数:"+getLength(singleLikedlist.getHead())); } //方法:获取到单链表的节点的个数(如果是带头节点的链表,需求不统计头节点) public static int getLength(HeroNode head){ if (head.next == null){//空节点 return 0; } int length = 0; //定义一个辅助的变量,这里没有统计头节点 HeroNode cur = head.next; while (cur != null){ length++; cur = cur.next; } return length; } } //定义singleLinkedlist 管理我们的英雄 class SingleLikedlist{ //先初始化一个头节点 private HeroNode head = new HeroNode(0,"",""); //返回头节点 public HeroNode getHead() { return head; } //添加节点到单项节点 //当不考虑编号的顺序时,找到当前链表的最后节点,将最后这个节点next指向新节点 public void add(HeroNode heroNode){ //因为head节点不能动,因此我们需要一个辅助变量 HeroNode temp =head; while (true){ if(temp.next == null){ break; } //没有到最后 temp = temp.next; } //当退出while循环,temp指向链表最后 //将最后这个节点的next指向新的节点 temp.next = heroNode;//将最后一个节点的next指向新加入的点 } //第二种方式在添加英雄时,根据排名将英雄插入到指定位置 //(如果有这个排名,则添加失败,并给出提示) public void addByOeder(HeroNode heroNode){ //因为头节点不能动,因此我们仍然通过一个辅助指针(变量)来帮助找到添加的位置 //因为单链表,因为我们找的temp是位于添加位置的前一个节点,否则插入不了 HeroNode temp = head; boolean flag = 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;//说明编号存在 } temp = temp.next;//后移,遍历当前链表 } //判断flag的值 if(flag){ System.out.printf("准备插入的英雄的编号%d 已经存在了,不能加入\n",heroNode.no); }else { //插入到链表中,temp的后面 heroNode.next = temp.next; temp.next = heroNode; } } //修改节点的信息,根据no编号来修改,即no编号不能改 //说明 //1.根据 newHeroNode 的no来修改即可 public void update(HeroNode newheroNode){ //判断是否为空 if(head.next == null){ System.out.println("链表为空~"); return; } //找到需要修改的节点,根据no编号 //定义一个辅助变量 HeroNode temp = head.next; boolean flag = false;//表示是否找到该节点 while (true){ if(temp == null){ break;//已经遍历完链表 } if(temp.no == newheroNode.no){ //找到 flag = true; break; } temp = temp.next; } //根据flag 判断是否找到修改的节点 if(flag){ temp.name = newheroNode.name; temp.nickname = newheroNode.nickname; }else{//没有找到 System.out.printf("没有找到编号%d的节点,不能修改\n",newheroNode.no); } } //删除节点 //思路 //1. head 不能动,因此我们需要一个temp辅助节点找到待删除的前一个节点 //2. 说明我们在比较时,是temp.next.no 和 需要删除的节点的no比较 public void del(int no){ HeroNode temp = head; boolean flag = false;//标志是否找到待删除节点的 while(true){ if (temp.next == null){//已经到了链表的最后 break; } if (temp.next.no == no){ //找到的待删除节点的前一个节点temp flag = true; break; } temp = temp.next;//temp后移,遍历 } //判断flag if (flag){//找到 //可以删除 temp.next = temp.next.next; }else{ System.out.printf("要删除的%d 节点不存在\n",no); } } //显示链表[遍历] public void showList(){ //先判断链表是否为空 if(head.next == null){ return; } HeroNode temp = head.next; while(true){ //是否到链表最后 if(temp == null){ break; } //输出节点信息 System.out.println(temp); temp = temp.next; } } } //定义一个HeroNode,每一个HeroNode就是一个节点 class HeroNode{ //定义节点的私有属性 编号 姓名 外号 和next public int no; public String name; public String nickname; public HeroNode next; //构造器 public HeroNode(){ } public HeroNode(int no,String name, String nickname){ this.no = no; this.name = name; this.nickname = nickname; } //重写toString 方便显示 public String toString(){ return "Heronode [no= "+no+",name="+name+",nickname="+nickname+"]"; } }
-
-
求单链表的倒数第K个节点
-
public class zezhen99 { 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(3, "zezhen", ""); //创建一个链表 SingleLikedlist singleLikedlist = new SingleLikedlist(); singleLikedlist.addByOeder(heroNode3); singleLikedlist.addByOeder(heroNode1); singleLikedlist.addByOeder(heroNode2); singleLikedlist.update(heroNode4); singleLikedlist.del(2); singleLikedlist.showList(); System.out.println("有效的节点个数:" + getLength(singleLikedlist.getHead())); HeroNode res = findLastIndexNode(singleLikedlist.getHead(), 2); System.out.println(res); } //查找单链表中的倒数第K个节点 //思路 //1.编写一个方法,接收head节点,同时接收一个index //2.index 表示是倒数第index个节点 //3.先把链表从头到尾遍历,看到链表的总的长度getLength //4.size后,我们从链表的第一个开始遍历(size-index)个,就可以看到 //5.如果找到了,则返回该节点,否则返回null public static HeroNode findLastIndexNode(HeroNode head, int index) { //判断如果链表为空,返回null if (head.next == null) { return null;//没有找到 } //第一个遍历得到链表的长度(节点长度) int size = getLength(head); //第二次遍历 size-index 位置,就是我们倒数的第K个节点 //先做一个index的校验 if (index <= 0 || index > size) { return null; } //定义给辅助变量,for循环定位到倒数的index HeroNode cur = head.next; for (int i = 0; i < size - index; i++) { cur = cur.next; } return cur; } //方法:获取到单链表的节点的个数(如果是带头节点的链表,需求不统计头节点) public static int getLength(HeroNode head) { if (head.next == null) {//空节点 return 0; } int length = 0; //定义一个辅助的变量,这里没有统计头节点 HeroNode cur = head.next; while (cur != null) { length++; cur = cur.next; } return length; } } //定义singleLinkedlist 管理我们的英雄 class SingleLikedlist { //先初始化一个头节点 private HeroNode head = new HeroNode(0, "", ""); //返回头节点 public HeroNode getHead() { return head; } //添加节点到单项节点 //当不考虑编号的顺序时,找到当前链表的最后节点,将最后这个节点next指向新节点 public void add(HeroNode heroNode) { //因为head节点不能动,因此我们需要一个辅助变量 HeroNode temp = head; while (true) { if (temp.next == null) { break; } //没有到最后 temp = temp.next; } //当退出while循环,temp指向链表最后 //将最后这个节点的next指向新的节点 temp.next = heroNode;//将最后一个节点的next指向新加入的点 } //第二种方式在添加英雄时,根据排名将英雄插入到指定位置 //(如果有这个排名,则添加失败,并给出提示) public void addByOeder(HeroNode heroNode) { //因为头节点不能动,因此我们仍然通过一个辅助指针(变量)来帮助找到添加的位置 //因为单链表,因为我们找的temp是位于添加位置的前一个节点,否则插入不了 HeroNode temp = head; boolean flag = 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;//说明编号存在 } temp = temp.next;//后移,遍历当前链表 } //判断flag的值 if (flag) { System.out.printf("准备插入的英雄的编号%d 已经存在了,不能加入\n", heroNode.no); } else { //插入到链表中,temp的后面 heroNode.next = temp.next; temp.next = heroNode; } } //修改节点的信息,根据no编号来修改,即no编号不能改 //说明 //1.根据 newHeroNode 的no来修改即可 public void update(HeroNode newheroNode) { //判断是否为空 if (head.next == null) { System.out.println("链表为空~"); return; } //找到需要修改的节点,根据no编号 //定义一个辅助变量 HeroNode temp = head.next; boolean flag = false;//表示是否找到该节点 while (true) { if (temp == null) { break;//已经遍历完链表 } if (temp.no == newheroNode.no) { //找到 flag = true; break; } temp = temp.next; } //根据flag 判断是否找到修改的节点 if (flag) { temp.name = newheroNode.name; temp.nickname = newheroNode.nickname; } else {//没有找到 System.out.printf("没有找到编号%d的节点,不能修改\n", newheroNode.no); } } //删除节点 //思路 //1. head 不能动,因此我们需要一个temp辅助节点找到待删除的前一个节点 //2. 说明我们在比较时,是temp.next.no 和 需要删除的节点的no比较 public void del(int no) { HeroNode temp = head; boolean flag = false;//标志是否找到待删除节点的 while (true) { if (temp.next == null) {//已经到了链表的最后 break; } if (temp.next.no == no) { //找到的待删除节点的前一个节点temp flag = true; break; } temp = temp.next;//temp后移,遍历 } //判断flag if (flag) {//找到 //可以删除 temp.next = temp.next.next; } else { System.out.printf("要删除的%d 节点不存在\n", no); } } //显示链表[遍历] public void showList() { //先判断链表是否为空 if (head.next == null) { return; } HeroNode temp = head.next; while (true) { //是否到链表最后 if (temp == null) { break; } //输出节点信息 System.out.println(temp); temp = temp.next; } } } //定义一个HeroNode,每一个HeroNode就是一个节点 class HeroNode { //定义节点的私有属性 编号 姓名 外号 和next public int no; public String name; public String nickname; public HeroNode next; //构造器 public HeroNode() { } public HeroNode(int no, String name, String nickname) { this.no = no; this.name = name; this.nickname = nickname; } //重写toString 方便显示 public String toString() { return "Heronode [no= " + no + ",name=" + name + ",nickname=" + nickname + "]"; } }
-
-
反转链表,有点难度
-
public class zezhen99 { 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(3, "zezhen", ""); //创建一个链表 SingleLikedlist singleLikedlist = new SingleLikedlist(); singleLikedlist.addByOeder(heroNode3); singleLikedlist.addByOeder(heroNode1); singleLikedlist.addByOeder(heroNode2); singleLikedlist.update(heroNode4); singleLikedlist.del(2); singleLikedlist.showList(); System.out.println("有效的节点个数:" + getLength(singleLikedlist.getHead())); HeroNode res = findLastIndexNode(singleLikedlist.getHead(), 2); System.out.println(res); //测试单链表的反转 reversetList(singleLikedlist.getHead()); singleLikedlist.showList(); } //将单链表反转 public static void reversetList(HeroNode head){ //如果当前链表为空,或者只有一个节点,无需反转,直接返回 if (head.next == null || head.next.next == null){ return ; } //定义一个辅助的指针(变量),帮助我们遍历原来的链表 HeroNode cur = head.next; HeroNode next = null;// 指向当前节点(cur)的下一个节点 HeroNode reverseHead = new HeroNode(0,"",""); //遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead 的最前端 while (cur != null){ next = cur.next;//先暂时保存当前节点的下一个节点,因为后面需要使用 cur.next = reverseHead.next;//将cur的下一个节点指向新的链表的最前端 reverseHead.next = cur;//将cur连接到新的链表上 cur = next;//让cur后移 } //将head.next指向reverseHead.next,实现单链表的反转 head.next = reverseHead.next; } //查找单链表中的倒数第K个节点 //思路 //1.编写一个方法,接收head节点,同时接收一个index //2.index 表示是倒数第index个节点 //3.先把链表从头到尾遍历,看到链表的总的长度getLength //4.size后,我们从链表的第一个开始遍历(size-index)个,就可以看到 //5.如果找到了,则返回该节点,否则返回null public static HeroNode findLastIndexNode(HeroNode head, int index) { //判断如果链表为空,返回null if (head.next == null) { return null;//没有找到 } //第一个遍历得到链表的长度(节点长度) int size = getLength(head); //第二次遍历 size-index 位置,就是我们倒数的第K个节点 //先做一个index的校验 if (index <= 0 || index > size) { return null; } //定义给辅助变量,for循环定位到倒数的index HeroNode cur = head.next; for (int i = 0; i < size - index; i++) { cur = cur.next; } return cur; } //方法:获取到单链表的节点的个数(如果是带头节点的链表,需求不统计头节点) public static int getLength(HeroNode head) { if (head.next == null) {//空节点 return 0; } int length = 0; //定义一个辅助的变量,这里没有统计头节点 HeroNode cur = head.next; while (cur != null) { length++; cur = cur.next; } return length; } } //定义singleLinkedlist 管理我们的英雄 class SingleLikedlist { //先初始化一个头节点 private HeroNode head = new HeroNode(0, "", ""); //返回头节点 public HeroNode getHead() { return head; } //添加节点到单项节点 //当不考虑编号的顺序时,找到当前链表的最后节点,将最后这个节点next指向新节点 public void add(HeroNode heroNode) { //因为head节点不能动,因此我们需要一个辅助变量 HeroNode temp = head; while (true) { if (temp.next == null) { break; } //没有到最后 temp = temp.next; } //当退出while循环,temp指向链表最后 //将最后这个节点的next指向新的节点 temp.next = heroNode;//将最后一个节点的next指向新加入的点 } //第二种方式在添加英雄时,根据排名将英雄插入到指定位置 //(如果有这个排名,则添加失败,并给出提示) public void addByOeder(HeroNode heroNode) { //因为头节点不能动,因此我们仍然通过一个辅助指针(变量)来帮助找到添加的位置 //因为单链表,因为我们找的temp是位于添加位置的前一个节点,否则插入不了 HeroNode temp = head; boolean flag = 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;//说明编号存在 } temp = temp.next;//后移,遍历当前链表 } //判断flag的值 if (flag) { System.out.printf("准备插入的英雄的编号%d 已经存在了,不能加入\n", heroNode.no); } else { //插入到链表中,temp的后面 heroNode.next = temp.next; temp.next = heroNode; } } //修改节点的信息,根据no编号来修改,即no编号不能改 //说明 //1.根据 newHeroNode 的no来修改即可 public void update(HeroNode newheroNode) { //判断是否为空 if (head.next == null) { System.out.println("链表为空~"); return; } //找到需要修改的节点,根据no编号 //定义一个辅助变量 HeroNode temp = head.next; boolean flag = false;//表示是否找到该节点 while (true) { if (temp == null) { break;//已经遍历完链表 } if (temp.no == newheroNode.no) { //找到 flag = true; break; } temp = temp.next; } //根据flag 判断是否找到修改的节点 if (flag) { temp.name = newheroNode.name; temp.nickname = newheroNode.nickname; } else {//没有找到 System.out.printf("没有找到编号%d的节点,不能修改\n", newheroNode.no); } } //删除节点 //思路 //1. head 不能动,因此我们需要一个temp辅助节点找到待删除的前一个节点 //2. 说明我们在比较时,是temp.next.no 和 需要删除的节点的no比较 public void del(int no) { HeroNode temp = head; boolean flag = false;//标志是否找到待删除节点的 while (true) { if (temp.next == null) {//已经到了链表的最后 break; } if (temp.next.no == no) { //找到的待删除节点的前一个节点temp flag = true; break; } temp = temp.next;//temp后移,遍历 } //判断flag if (flag) {//找到 //可以删除 temp.next = temp.next.next; } else { System.out.printf("要删除的%d 节点不存在\n", no); } } //显示链表[遍历] public void showList() { //先判断链表是否为空 if (head.next == null) { return; } HeroNode temp = head.next; while (true) { //是否到链表最后 if (temp == null) { break; } //输出节点信息 System.out.println(temp); temp = temp.next; } } } //定义一个HeroNode,每一个HeroNode就是一个节点 class HeroNode { //定义节点的私有属性 编号 姓名 外号 和next public int no; public String name; public String nickname; public HeroNode next; //构造器 public HeroNode() { } public HeroNode(int no, String name, String nickname) { this.no = no; this.name = name; this.nickname = nickname; } //重写toString 方便显示 public String toString() { return "Heronode [no= " + no + ",name=" + name + ",nickname=" + nickname + "]"; } }
-
从尾到头打印单链表(百度面试题)(运用到栈的知识)
-
-
import java.util.Stack; public class zezhen99 { 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(3, "zezhen", ""); //创建一个链表 SingleLikedlist singleLikedlist = new SingleLikedlist(); singleLikedlist.addByOeder(heroNode3); singleLikedlist.addByOeder(heroNode1); singleLikedlist.addByOeder(heroNode2); singleLikedlist.update(heroNode4); singleLikedlist.del(2); singleLikedlist.showList(); System.out.println("有效的节点个数:" + getLength(singleLikedlist.getHead())); HeroNode res = findLastIndexNode(singleLikedlist.getHead(), 2); System.out.println(res); //测试单链表的反转 reversetList(singleLikedlist.getHead()); singleLikedlist.showList(); //逆序打印 reversePrint(singleLikedlist.getHead()); } //使用方式2来逆序打印(栈) public static void reversePrint(HeroNode head){ if (head.next == null){ return; } //创建要给一个栈,将各个节点压入栈 Stack<HeroNode> stack = new Stack<HeroNode>(); HeroNode cur = head.next; //将链表的所有节点压入栈 while (cur != null){ stack.push(cur); cur = cur.next;//cur后移,这样就可以压入下一个节点 } //将栈中的节点进行打印,pop出栈 while (stack.size() >0){ System.out.println(stack.pop()); } } //将单链表反转 public static void reversetList(HeroNode head){ //如果当前链表为空,或者只有一个节点,无需反转,直接返回 if (head.next == null || head.next.next == null){ return ; } //定义一个辅助的指针(变量),帮助我们遍历原来的链表 HeroNode cur = head.next; HeroNode next = null;// 指向当前节点(cur)的下一个节点 HeroNode reverseHead = new HeroNode(0,"",""); //遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead 的最前端 while (cur != null){ next = cur.next;//先暂时保存当前节点的下一个节点,因为后面需要使用 cur.next = reverseHead.next;//将cur的下一个节点指向新的链表的最前端 reverseHead.next = cur;//将cur连接到新的链表上 cur = next;//让cur后移 } //将head.next指向reverseHead.next,实现单链表的反转 head.next = reverseHead.next; } //查找单链表中的倒数第K个节点 //思路 //1.编写一个方法,接收head节点,同时接收一个index //2.index 表示是倒数第index个节点 //3.先把链表从头到尾遍历,看到链表的总的长度getLength //4.size后,我们从链表的第一个开始遍历(size-index)个,就可以看到 //5.如果找到了,则返回该节点,否则返回null public static HeroNode findLastIndexNode(HeroNode head, int index) { //判断如果链表为空,返回null if (head.next == null) { return null;//没有找到 } //第一个遍历得到链表的长度(节点长度) int size = getLength(head); //第二次遍历 size-index 位置,就是我们倒数的第K个节点 //先做一个index的校验 if (index <= 0 || index > size) { return null; } //定义给辅助变量,for循环定位到倒数的index HeroNode cur = head.next; for (int i = 0; i < size - index; i++) { cur = cur.next; } return cur; } //方法:获取到单链表的节点的个数(如果是带头节点的链表,需求不统计头节点) public static int getLength(HeroNode head) { if (head.next == null) {//空节点 return 0; } int length = 0; //定义一个辅助的变量,这里没有统计头节点 HeroNode cur = head.next; while (cur != null) { length++; cur = cur.next; } return length; } } //定义singleLinkedlist 管理我们的英雄 class SingleLikedlist { //先初始化一个头节点 private HeroNode head = new HeroNode(0, "", ""); //返回头节点 public HeroNode getHead() { return head; } //添加节点到单项节点 //当不考虑编号的顺序时,找到当前链表的最后节点,将最后这个节点next指向新节点 public void add(HeroNode heroNode) { //因为head节点不能动,因此我们需要一个辅助变量 HeroNode temp = head; while (true) { if (temp.next == null) { break; } //没有到最后 temp = temp.next; } //当退出while循环,temp指向链表最后 //将最后这个节点的next指向新的节点 temp.next = heroNode;//将最后一个节点的next指向新加入的点 } //第二种方式在添加英雄时,根据排名将英雄插入到指定位置 //(如果有这个排名,则添加失败,并给出提示) public void addByOeder(HeroNode heroNode) { //因为头节点不能动,因此我们仍然通过一个辅助指针(变量)来帮助找到添加的位置 //因为单链表,因为我们找的temp是位于添加位置的前一个节点,否则插入不了 HeroNode temp = head; boolean flag = 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;//说明编号存在 } temp = temp.next;//后移,遍历当前链表 } //判断flag的值 if (flag) { System.out.printf("准备插入的英雄的编号%d 已经存在了,不能加入\n", heroNode.no); } else { //插入到链表中,temp的后面 heroNode.next = temp.next; temp.next = heroNode; } } //修改节点的信息,根据no编号来修改,即no编号不能改 //说明 //1.根据 newHeroNode 的no来修改即可 public void update(HeroNode newheroNode) { //判断是否为空 if (head.next == null) { System.out.println("链表为空~"); return; } //找到需要修改的节点,根据no编号 //定义一个辅助变量 HeroNode temp = head.next; boolean flag = false;//表示是否找到该节点 while (true) { if (temp == null) { break;//已经遍历完链表 } if (temp.no == newheroNode.no) { //找到 flag = true; break; } temp = temp.next; } //根据flag 判断是否找到修改的节点 if (flag) { temp.name = newheroNode.name; temp.nickname = newheroNode.nickname; } else {//没有找到 System.out.printf("没有找到编号%d的节点,不能修改\n", newheroNode.no); } } //删除节点 //思路 //1. head 不能动,因此我们需要一个temp辅助节点找到待删除的前一个节点 //2. 说明我们在比较时,是temp.next.no 和 需要删除的节点的no比较 public void del(int no) { HeroNode temp = head; boolean flag = false;//标志是否找到待删除节点的 while (true) { if (temp.next == null) {//已经到了链表的最后 break; } if (temp.next.no == no) { //找到的待删除节点的前一个节点temp flag = true; break; } temp = temp.next;//temp后移,遍历 } //判断flag if (flag) {//找到 //可以删除 temp.next = temp.next.next; } else { System.out.printf("要删除的%d 节点不存在\n", no); } } //显示链表[遍历] public void showList() { //先判断链表是否为空 if (head.next == null) { return; } HeroNode temp = head.next; while (true) { //是否到链表最后 if (temp == null) { break; } //输出节点信息 System.out.println(temp); temp = temp.next; } } } //定义一个HeroNode,每一个HeroNode就是一个节点 class HeroNode { //定义节点的私有属性 编号 姓名 外号 和next public int no; public String name; public String nickname; public HeroNode next; //构造器 public HeroNode() { } public HeroNode(int no, String name, String nickname) { this.no = no; this.name = name; this.nickname = nickname; } //重写toString 方便显示 public String toString() { return "Heronode [no= " + no + ",name=" + name + ",nickname=" + nickname + "]"; } }
-
-
-
栈的初步了解利于理解第四步
-
import java.util.Stack; public class zezhen6 { public static void main(String[] args) { Stack<String> stack = new Stack<String>(); //入栈 stack.add("jack"); stack.add("tom"); stack.add("smith"); //出栈 while (stack.size()>0){ System.out.println(stack.pop()); } } }
-
- 温馨提示:每个代码中都包含的之前的代码,而新增加的就是我们这一步最核心的代码,个人建议将每一步理解了再看后续代码!!!
双向链表
管理单向链表的缺点分析
- 单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。
- 单向链表不能自我删除,需要靠辅助节点 ,而双向链表,则可以自我删除,所以前面我们单链表删除时节点,总是找到 temp,temp 是待删除节点的前一个节点(认真体会)。
- 分析了双向链表如何完成遍历,添加,修改和删除的思路
双向链表的代码实现
public class zezhen6 {
public static void main(String[] args) {
//测试
System.out.println("双向链表的测试");
//先创建节点
HeroNode2 heroNode1 = new HeroNode2(1, "", "");
HeroNode2 heroNode2 = new HeroNode2(2, "", "");
HeroNode2 heroNode3 = new HeroNode2(3, "", "");
//创建一个双向链表
DoubleLinkedList doubleLinkedList = new DoubleLinkedList();
doubleLinkedList.add(heroNode1);
doubleLinkedList.add(heroNode2);
doubleLinkedList.add(heroNode3);
doubleLinkedList.showList();
//修改
HeroNode2 newHeroNode = new HeroNode2(3, "zz", "798");
doubleLinkedList.update(newHeroNode);
System.out.println("修改后的链表情况");
doubleLinkedList.showList();
doubleLinkedList.del(1);
System.out.println("删除后");
doubleLinkedList.showList();
}
}
class DoubleLinkedList {
//先初始化一个头节点
private HeroNode2 head = new HeroNode2(0, "", "");
//返回头节点
public HeroNode2 getHead() {
return head;
}
//遍历双向链表的方法
public void showList() {
//先判断链表是否为空
if (head.next == null) {
return;
}
HeroNode2 temp = head.next;
while (true) {
//是否到链表最后
if (temp == null) {
break;
}
//输出节点信息
System.out.println(temp);
temp = temp.next;
}
}
//添加一个节点到双向链表的最后
public void add(HeroNode2 heroNode) {
//因为head节点不能动,因此我们需要一个辅助变量
HeroNode2 temp = head;
while (true) {
if (temp.next == null) {
break;
}
//没有到最后
temp = temp.next;
}
//当退出while循环,temp指向链表最后
//形成一个双向链表
temp.next = heroNode;
heroNode.pre = temp;
}
//修改一个节点内容
public void update(HeroNode2 newheroNode) {
//判断是否为空
if (head.next == null) {
System.out.println("链表为空~");
return;
}
//找到需要修改的节点,根据no编号
//定义一个辅助变量
HeroNode2 temp = head.next;
boolean flag = false;//表示是否找到该节点
while (true) {
if (temp == null) {
break;//已经遍历完链表
}
if (temp.no == newheroNode.no) {
//找到
flag = true;
break;
}
temp = temp.next;
}
//根据flag 判断是否找到修改的节点
if (flag) {
temp.name = newheroNode.name;
temp.nickname = newheroNode.nickname;
} else {//没有找到
System.out.printf("没有找到编号%d的节点,不能修改\n", newheroNode.no);
}
}
//从双向链表中删除一个节点
//说明1 对于双向链表,我们可以直接找到要删除的节点
//2找到后, 自我删除即可
public void del(int no) {
//判断当前链表为空
if (head.next == null) {//空链表
System.out.println("链表为空,无法删除");
return;
}
HeroNode2 temp = head.next;//辅助变量
boolean flag = false;//标志是否找到待删除节点的
while (true) {
if (temp == null) {//已经到了链表的最后
break;
}
if (temp.no == no) {
//找到的待删除节点temp
flag = true;
break;
}
temp = temp.next;//temp后移,遍历
}
//判断flag
if (flag) {//找到
//可以删除
temp.pre.next = temp.next;
//如果是最后一个节点,就不需要执行下面这句话,否则出现空指针
if (temp.next != null) {
temp.next.pre = temp.pre;
}
} else {
System.out.printf("要删除的%d 节点不存在\n", no);
}
}
}
//定义HeroNode2, 每个HeroNode 对象就是一个节点
class HeroNode2 {
//定义节点的私有属性 编号 姓名 外号 和next
public int no;
public String name;
public String nickname;
public HeroNode2 next;//指向下一个节点
public HeroNode2 pre;//指向前一个节点
//构造器
public HeroNode2() {
}
public HeroNode2(int no, String name, String nickname) {
this.no = no;
this.name = name;
this.nickname = nickname;
}
//重写toString 方便显示
public String toString() {
return "Heronode [no= " + no + ",name=" + name + ",nickname=" + nickname + "]";
}
}
单向环形链表
Josephu(约瑟夫、约瑟夫环) 问题
- Josephu 问题为:设编号为 1,2,… n 的 n 个人围坐一圈,约定编号为 k(1<=k<=n)的人从 1 开始报数,数到 m 的那个人出列,它的下一位又从 1 开始报数,数到 m 的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
- 提示:用一个不带头结点的循环链表来处理 Josephu 问题:先构成一个有 n 个结点的单循环链表,然后由 k 结点起从 1 开始计数,计到m 时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从 1 开始计数,直 到最后一个结点从链表中删除算法结束
代码实现
import java.util.Scanner;
public class zezhen11 {
public static void main(String[] args) {
//测试一下
CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
circleSingleLinkedList.addBoy(5);
circleSingleLinkedList.showBoy();
circleSingleLinkedList.countBoy(1,2,5);
}
}
//创建一个环形的单向链表
class CircleSingleLinkedList{
//创建一个first节点,当前没有编号
private Boy first = null;
//添加小孩节点,构建一个环形链表
public void addBoy(int nums){
//nums 做一个数据校验
if (nums < 1){
System.out.println("nums的值不正确");
return;
}
Boy curBoy = null;//辅助指针,帮助构建环形链表
//使用for来创建我们的环形链表
for (int i = 1; i <= nums; i++){
//根据编号,创建小孩节点
Boy boy = 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 showBoy(){
//判断链表是否为空
if (first == null){
System.out.println("没有任何小孩~");
return;
}
//因为first不能动,因此我们仍然使用一个辅助指针完成遍历
Boy curBoy = first;
while (true){
System.out.printf("小孩的编号 %d \n", curBoy.getNo());
if (curBoy.getNext() == first){//说明已经遍历完毕
break;
}
curBoy = curBoy.getNext();//curBoy后移
}
}
public void countBoy(int startNo,int countNum,int nums){
//先对数据进行校验
if (first == null || startNo < 1 || startNo > nums){
System.out.println("参数输入有误,请重新输入");
return;
}
//创建要给辅助指针,帮助完成小孩出圈
Boy helper = first;
//需创建一个辅助指针(变量) helper,事先应该指向环形链表的最后这个节点
while (true){
if (helper.getNext() == first){//说明helper指向最后小孩节点
break;
}
helper = helper.getNext();
}
//小孩报数前,先让first 和 helper 移动k-1次
for (int j = 0; j<startNo-1;j++){
first = first.getNext();
helper = helper.getNext();
}
//当小孩报数时,让first和helper指针同时移动m-1次,然后出圈
//这里是一个循环操作,知道圈中只有一个节点
while (true){
if (helper == first){//说明圈中只有一个节点
break;
}
//这时first和helper指针同时移动countNum - 1
for(int j = 0; j < countNum - 1; j++){
first = first.getNext();
helper = helper.getNext();
}
//这时first指向的节点,就是要出圈小孩的节点
System.out.printf("小孩%d出圈\n",first.getNo());
//这时将first指向的小孩节点出圈
first = first.getNext();
helper.setNext(first);
}
System.out.printf("最后留在圈中的小孩编号%d\n",first.getNo());
}
}
//创建一个Boy类,表示一个节点
class Boy{
private int no;//编号
private Boy next;//指向下一个节点,默认null
public Boy(int no) {
this.no = no;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public Boy getNext() {
return next;
}
public void setNext(Boy next) {
this.next = next;
}
}
栈
栈的介绍
- 栈的英文为(stack)
- 栈是一个先入后出(FILO-First In Last Out)的有序列表。
- 栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的 一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)。
- 根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元 素最先删除,最先放入的元素最后删除
- 图解方式说明出栈(pop)和入栈(push)的概念
栈的应用场景
- 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。
- 处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆栈中。
- 表达式的转换[中缀表达式转后缀表达式]与求值(实际解决)。
- 二叉树的遍历。
- 图形的深度优先(depth 一 first)搜索法。
利用数组模拟栈
代码实现
import java.util.Scanner;
public class ArrayStackDemo {
public static void main(String[] args) {
//测试一下ArrayStack 是否正确
//先创建一个ArrayStack对象->表示栈
ArrayStack stack = new ArrayStack(4);
String key = "";
boolean loop = true;//控制是否退出菜单
Scanner scanner = new Scanner(System.in);
while (loop) {
System.out.println("show: 表示显示栈");
System.out.println("exit: 退出程序");
System.out.println("push: 表示添加数据到栈(入栈)");
System.out.println("pop: 表示从栈取出数据(出栈)");
System.out.println("请输入你的选择");
key = scanner.next();
switch (key) {
case "show":
stack.list();
break;
case "push":
System.out.println("请输入一个数");
int value = scanner.nextInt();
stack.push(value);
break;
case "pop":
try {
int res = stack.pop();
System.out.printf("出栈的数据是%d\n",res);
}catch (Exception e){
System.out.println(e.getMessage());
}
break;
case "exit":
scanner.close();
loop = false;
break;
default:
break;
}
}
System.out.println("程序退出~~");
}
}
//定义一个ArrayStack 表示栈
class ArrayStack {
private int maxSize;//栈的大小
private int[] stack;//数组,数组模拟栈,数据就放在该数组
private int top = -1;//top表示栈顶,初始化为-1
//构造器
public ArrayStack(int maxSize) {
this.maxSize = maxSize;
stack = new int[this.maxSize];
}
//栈满
public boolean isFull() {
return top == maxSize - 1;
}
//栈空
public boolean isEmpty() {
return top == -1;
}
//入栈-push
public void push(int value) {
//先判断栈是否满
if (isFull()) {
System.out.println("栈满");
return;
}
top++;
stack[top] = value;
}
//出栈-pop, 将栈顶的数据返回
public int pop() {
//先判断是否空
if (isEmpty()) {
//抛出异常
throw new RuntimeException("栈空,没有数据~");
}
int value = stack[top];
top--;
return value;
}
//显示栈的情况[遍历栈],遍历时,需要从栈顶开始显示数据
public void list() {
if (isEmpty()) {
System.out.println("栈空,没有数据~~");
return;
}
//需要从栈顶开始显示数据
for (int i = top; i >= 0; i--) {
System.out.printf("stack[%d]=%d\n",i, stack[i]);
}
}
}
栈实现综合计算器(中缀表达式)
代码实现[1.先实现一位数的运算,2.扩展到多位数的运算]
public class zezhen10 {
public static void main(String[] args) {
// 根据前面老师思路,完成表达式的运算
String expression = "70+2*6-2";
// 创建两个栈,数栈,一个符号栈
ArrayStack2 numStack = new ArrayStack2(10);
ArrayStack2 operStack = new ArrayStack2(10);
// 定义需要的相关变量
int index = 0;// 用于扫描
int num1 = 0, num2 = 0;
int oper = 0;
int res = 0;
char ch = ' ';// 将每次扫描得到char保存到ch
String keepNum = "";//用于拼接多位数
// 开始while循环的扫描expression
while (true) {
// 依次得到expression 的每一个字符
ch = expression.substring(index, index + 1).charAt(0);
// 判断当前ch是什么,然后做相应的处理
if (operStack.isOper(ch)) {
// 判断当前的符号栈是否为空
if (!operStack.isEmpty()) {
// 如果符号栈有操作符,就进行比较,如果当前的操作符的优先级小于或者等于栈中的操作符,就需要从数栈中pop出两个数
// 在从符号栈中pop出一个符号,进行运算,将得到结果,入数栈,然后将当前的操作符入符号栈
if (operStack.priority(ch) <= operStack.priority(operStack.peek()) ) {
num1 = numStack.pop();
num2 = numStack.pop();
oper = operStack.pop();
res = operStack.cal(num1, num2, oper);
// 把运算的结果如数栈
numStack.push(res);
// 然后将当前的操作符入符号栈
operStack.push(ch);
} else {
// 如果当前的操作符的优先级大于栈中的操作符,就直接入符号栈
operStack.push(ch);
}
} else {
// 如果为空直接入符号栈
operStack.push(ch);
}
} else {//如果是数,则直接入数栈
//1.当处理多位数时,不能发现是一个数就立即入栈,因为·他可能是多位数
//2.在处理数时,需要向expression的表达式的index后再看一位,如果是数就进行扫描,如果是符号才入栈
//3. 因此我们需要定义一个变量字符串,用于拼接
//numStack.push(ch-48);
//处理多位数
keepNum += ch;
//如果ch已经是expression的最后一位,就直接入栈
if (index == expression.length()-1){
numStack.push(Integer.parseInt(keepNum));
}else {
//判断下一个字符是不是数字,如果是数字,就继续扫描,如果是运算符,则入栈
//注意是看后一位,不是index++
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 = operStack.cal(num1, num2, oper);
numStack.push(res);
}
//将数栈的最后数,pop出,就是结果
System.out.printf("表达式: %s = %d",expression,numStack.pop());
}
}
//先创建一个栈,直接使用前面创建好的
//定义一个ArrayStack2 表示栈,需要扩展功能
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];
}
// 增加一个方法,可以返回当前栈顶的值,但不是真正的pop
public int peek() {
return stack[top];
}
// 栈满
public boolean isFull() {
return top == maxSize - 1;
}
// 栈空
public boolean isEmpty() {
return top == -1;
}
// 入栈-push
public void push(int value) {
// 先判断栈是否满
if (isFull()) {
System.out.println("栈满");
return;
}
top++;
stack[top] = value;
}
// 出栈-pop, 将栈顶的数据返回
public int pop() {
// 先判断是否空
if (isEmpty()) {
// 抛出异常
throw new RuntimeException("栈空,没有数据~");
}
int value = stack[top];
top--;
return value;
}
// 显示栈的情况[遍历栈],遍历时,需要从栈顶开始显示数据
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 priority(int oper) {
if (oper == '*' || oper == '/') {
return 1;
} else if (oper == '+' || oper == '-') {
return 0;
} else {
return -1;
}
}
// 判断是不是一个运算符
public boolean isOper(char val) {
return val == '+' || val == '-' || val == '*' || val == '/';
}
// 计算方法
public int cal(int num1, int num2, int oper) {
int res = 0;// res 用于存放计算的结果
switch (oper) {
case '+':
res = num1 + num2;
break;
case '-':
res = num2 - num1;
break;
case '*':
res = num1 * num2;
break;
case '/':
res = num2 / num1;
break;
default:
break;
}
return res;
}
}
逆波兰计算器
我们完成一个逆波兰计算器,要求完成如下任务:
- 输入一个逆波兰表达式(后缀表达式),使用栈(Stack),计算其结果
- 支持小括号和多位数整数,因为这里我们主要将的是数据结构,因此计算器进行简化,只支持对整数的计算
- 思路分析
代码实现
import java.awt.*;
import java.time.chrono.ThaiBuddhistChronology;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.util.Stack;
public class zz {
public static void main(String[] args) {
//先定义个逆波兰表达式
//(3+4)*5-6 => 3 4 + 5 * 6 -
//说明为了方便,逆波兰表达式的数字和符号使用空格隔开
String suffixEepression = "3 4 + 5 * 6 -";
//思路
//1.先将"3 4 + 5 * 6 - " => 放到ArrayList中
//2.将ArrayList 传递给一个方法,遍历ArrayList 配合栈 完成计算
List<String> list = getListString(suffixEepression);
System.out.println("rpnList"+ list);
int res = calculate(list);
System.out.println("计算的结果是="+res);
}
//将一个逆波兰表达式,依次将数据和运算符放入到Arraylist中
public static List<String> getListString(String suffixExpression){
//将suffixExpression 分割
String[] split = suffixExpression.split(" ");
List<String> list = new ArrayList<String>();
for (String ele: split){
list.add(ele);
}
return list;
}
//完成对逆波兰表达式的运算
public static int calculate(List<String> ls){
//创建一个栈,只需要一个栈即可
Stack<String> stack = new Stack<String>();
//遍历 ls
for (String item : ls){
//这里使用正则表达式来取出数
if (item.matches("\\d+")){//匹配的是多位数
//入栈
stack.push(item);
}else{
//pop出两个数,并运算
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);//把一个整数转成字符串并加入栈
}
}
//最后留在stack中的数据是运算结果
return Integer.parseInt(stack.pop()) ;
}
}
中缀表达式转换为后缀表达式
思路理解
大家看到,后缀表达式适合计算式进行运算,但是人却不太容易写出来,尤其是表达式很长的情况下,因此在开发中,我们需要将中缀表达式转成后缀表达式。
- 初始化两个栈:运算符栈 s1 和储存中间结果的栈 s2;
- 从左至右扫描中缀表达式;
- 遇到操作数时,将其压 s2;
- 遇到运算符时,比较其与 s1 栈顶运算符的优先级:
- 如果 s1 为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈;
- 否则,若优先级比栈顶运算符的高,也将运算符压入 s1;
- 否则,将 s1 栈顶的运算符弹出并压入到 s2 中,再次转到(4.1)与 s1 中新的栈顶运算符相比较;
- 遇到括号时:
- 如果是左括号“(”,则直接压入 s1
- 如果是右括号“)”,则依次弹出 s1 栈顶的运算符,并压入 s2,直到遇到左括号为止,此时将这一对括号丢弃
- 重复步骤 2 至 5,直到表达式的最右边
- 将 s1 中剩余的运算符依次弹出并压入 s2
- 依次弹出 s2 中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式
代码实现
import java.awt.*;
import java.time.chrono.ThaiBuddhistChronology;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.util.Stack;
public class Test1 {
public static void main(String[] args) {
//完成将一个中缀表达式转成后缀表达式的功能
//说明
//1. 1+((2+3)*4)-5 => 转成 1 2 3 + 4 * + 5 -
//2.因为直接对string进行操作,不方便,因此先将“1+((2+3)*4)-5" => 中缀的表达式对应的List
// 即“ 1+((2+3)*4)-5” => ArrayList [1,+,(,(,2,+3,),*,4,),-,5]
String expression = "1+((2+3)*4)-5";
List<String> infixExpressionList = toInfixExpressionList(expression);
System.out.println("中缀表达式对应的List" + infixExpressionList);
List<String> SuffixExpressionList = parseSuffixExpressionList(infixExpressionList);
System.out.println("后缀表达式对应的List" + SuffixExpressionList);
System.out.printf("expression=%d", calculate(SuffixExpressionList));
System.out.println();
//先定义个逆波兰表达式
//(3+4)*5-6 => 3 4 + 5 * 6 -
//说明为了方便,逆波兰表达式的数字和符号使用空格隔开
String suffixEepression = "3 4 + 5 * 6 -";
//思路
//1.先将"3 4 + 5 * 6 - " => 放到ArrayList中
//2.将ArrayList 传递给一个方法,遍历ArrayList 配合栈 完成计算
List<String> list = getListString(suffixEepression);
System.out.println("rpnList" + list);
int res = calculate(list);
System.out.println("计算的结果是=" + res);
}
//即Arraylist [1,+,(,(,2,+3,),*,4,),-,5] => ArrayList [1,2,3,+,4,*,+,5,-]
//方法:将得到的中缀表达式对应的list => 后缀表达式对应的list
public static List<String> parseSuffixExpressionList(List<String> ls){
//定义两个栈
Stack<String> s1 = new Stack<String>();//符号栈
//说明:因为s2这个栈,在整个转换过程中,没有pop操作,而且后面我们还需要逆序输出
//所以不用stack而用list
List<String> s2 = new ArrayList<String>();//储存中间结果的lists2
//遍历ls
for (String item: ls){
//如果是一个数,加入s2
if (item.matches("\\d+")){
s2.add(item);
}else if (item.equals("(")){
s1.push(item);
}else if (item.equals(")")){
//如果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这对括号丢弃
while (!s1.peek().equals("(")){
s2.add(s1.pop());
}
s1.pop();//将(弹出s1栈,消除小括号
}else{
//优先级比较
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;//注意因为是存放到list,因此按顺序输出就是对应的后缀表达式对应的List
}
//方法:将中缀表达式转成对应的List
// s = "1+((2+3)*4)-5";
public static List<String> toInfixExpressionList(String s) {
//定义一个List,存放中缀表达式对应的内容
List<String> ls = new ArrayList<String>();
int i = 0;//这时是一个指针,用于遍历中缀表达式字符串
String str;//对多位数的拼接
char c; //每遍历到一个字符,就放到c
do {
//如果c是一个非数字,我需要加入到ls
if ((c = s.charAt(i)) < 48 || (c = s.charAt(i)) > 57) {
ls.add("" + c);
i++;//i需要后移
} else {//如果是一个数,需要考虑多位数
str = "";//先将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;//返回
}
//将一个逆波兰表达式,依次将数据和运算符放入到Arraylist中
public static List<String> getListString(String suffixExpression) {
//将suffixExpression 分割
String[] split = suffixExpression.split(" ");
List<String> list = new ArrayList<String>();
for (String ele : split) {
list.add(ele);
}
return list;
}
//完成对逆波兰表达式的运算
public static int calculate(List<String> ls) {
//创建一个栈,只需要一个栈即可
Stack<String> stack = new Stack<String>();
//遍历 ls
for (String item : ls) {
//这里使用正则表达式来取出数
if (item.matches("\\d+")) {//匹配的是多位数
//入栈
stack.push(item);
} else {
//pop出两个数,并运算
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);//把一个整数转成字符串并加入栈
}
}
//最后留在stack中的数据是运算结果
return Integer.parseInt(stack.pop());
}
}
//编写一个类Operation可以返回一个运算符对应的优先级
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 operation){
int result = 0;
switch (operation){
case "+":
result = ADD;
break;
case "-":
result = SUB;
break;
case "*":
result = MUL;
break;
case "/":
result = DIV;
break;
default:
System.out.println("不存在该运算符");
}
return result;
}
}
递归
应用场景
概念
简单的说: 递归就是方法自己调用自己,每次调用时传入不同的变量.递归有助于编程者解决复杂的问题,同时可以让代码变得简洁。
调用机制
若加入else,则输出一个2。
package com.atguigu.recusion;
public class RecusionTest01 {
public static void main(String[] args) {
//通过打印问题,回顾递归的调用机制
test(4);
}
public static void test(int n){
if(n > 2){
test(n -1);
}else {
System.out.println("n = " + n);
}
}
}
package com.atguigu.recusion;
public class RecusionTest01 {
public static void main(String[] args) {
//通过打印问题,回顾递归的调用机制
test(4);
System.out.println("res + " + factorial(2));
}
public static void test(int n){
if(n > 2){
test(n -1);
}
System.out.println("n = " + n);
}
//阶乘问题
public static int factorial(int n) {
if (n == 1) {
return 1;
} else {
return factorial(n - 1) * n; // 1 * 2 * 3
}
}
}
递归能解决什么样的问题
- 各种数学问题如: 8 皇后问题 , 汉诺塔, 阶乘问题, 迷宫问题, 球和篮子的问题(google 编程大赛)
- 各种算法中也会使用到递归,比如快排,归并排序,二分查找,分治算法等.
- 将用栈解决的问题–>递归代码比较简洁
递归需要遵守的规则
- 执行一个方法时,就创建一个新的受保护的独立空间(栈空间)
- 方法的局部变量是独立的,不会相互影响, 比如 n 变量
- 如果方法中使用的是引用类型变量(比如数组),就会共享该引用类型的数据.
- 递归必须向退出递归的条件逼近,否则就是无限递归,出现 StackOverflowError
- 当一个方法执行完毕,或者遇到 return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或 者返回时,该方法也就执行完毕
迷宫问题
代码实现
import java.util.Scanner;
public class Test2 {
public static void main(String[] args) {
//先创建一个二维数组,模拟迷宫
// 地图
int[][] map = new int[8][7];
//使用1 表示墙
//上下全部置为1
for (int i = 0; i < 7; i++) {
map[0][i] = 1;
map[7][i] = 1;
}
//左右全部置为1
for (int i = 0; i < 8; i++) {
map[i][0] = 1;
map[i][6] = 1;
}
//设置挡板, 1表示
map[3][1] = 1;
map[3][2] = 1;
//输出地图
System.out.println("地图的情况");
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 7; j++) {
System.out.print(map[i][j] + " ");
}
System.out.println();
}
//使用递归回溯给小球找路
setway(map,1,1);
//输出新的地图,小球走过,并标识过的递归
System.out.println("小球走过,并标识过的地图的情况");
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 7; j++) {
System.out.print(map[i][j] + " ");
}
System.out.println();
}
}
//使用递归回溯来给小球找路
//说明
//1.map表示地图
//2.i,j表示从地图的哪个位置开始出发(i,j)
//3.如果小球能到map[6][5]位置,则说明通路找到
//4.约定:当map[i][j]为0表示该点没有走过 当为1表示墙:2表示通路可以走;3表示该点已经走过走不通
//5.在走迷宫时,需要确定一个策略(方法)下->右->上->左,如果该点走不通再回溯
/**
*
* @param map 表示地图
* @param i 从哪个位置开始找
* @param j
* @return 如果找到通路,就返回true,否则返回false
*/
public static boolean setway(int[][] map, int i ,int j){
if(map[6][5] == 2){//通路已经找到ok
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;
}
}
}
}
对迷宫问题的讨论
- 小球得到的路径,和程序员设置的找路策略有关即:找路的上下左右的顺序相关
- 再得到小球路径时,可以先使用(下右上左),再改成(上右下左),看看路径是不是有变化
- 测试回溯现象
- 思考:如何求出最短路径,思路代码实现
八皇后
八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于 1848 年提出:在 8×8 格的国际象棋上摆放八个皇后,使其不能互相攻击,即:任意两个皇后都不能处于同一行、 同一列或同一斜线上,问有多少种摆法**(92)**。
解题思路
- 第一个皇后先放第一行第一列
- 第二个皇后放在第二行第一列,然后判断是否 OK, 如果不 OK,继续放在第二列、第三列、依次把所有列都 放完,找到一个合适
- 继续第三个皇后,还是第一列、第二列……直到第 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 列
代码实现
import java.util.Queue;
public class Test3 {
//定义一个max表示共有多少个皇后
int max = 8;
//定义数组array,保存皇后放置位置的结果,比如arr = {0,4,7,5,2,6,1,3}
int[] array = new int[max];
static int count = 0;
public static void main(String[] args) {
//测试一下
Test3 queue8 = new Test3();
queue8.check(0);
System.out.printf("一共有%d解法",count);
}
//编写一个方法,放置第n个皇后
//特别注意:check是每一次递归时,进入到check中有for(int i = 0; i < max; i++),因此会有回溯
private void check(int n){
if (n == max){//n = 8,其实8个皇后就已经放好
print();
return;
}
//依次放入皇后,并判断是否冲突
for (int i = 0; i < max; i++){
//先把当前这个皇后n,放到该行的第一列
array[n] = i;
//判断当放置第n个皇后到i列时,是否冲突
if (judge(n)){//不冲突
//接着放n+1个皇后,即开始递归
check(n+1);
}
//如果冲突,就继续执行array[n] = i;即将第n个皇后,放置在本行的后移的一个位置
}
}
//查看当我们放置第n个皇后,就去检测该皇后是否和前面已经摆放的皇后冲突
/**
* @param n
* @return
*/
private boolean judge(int n) {
for (int i = 0; i < n; i++) {
//说明
//1.array[i] == array[n] 表示判断第n个皇后是否和前面的n-1个皇后在同一列
//2.Math.abs(n-1) == Math.abs(array[n] - array[i]) 表示判断第n个皇后是否和第i皇后是否在同一斜线
//3.判断是否在同一行,没有必要,n每次都在递增
if (array[i] == array[n] || Math.abs(n - i) == Math.abs(array[n] - array[i])) {
return false;
}
}
return true;
}
//写一个方法,可以将皇后摆放的位置输出
public void print() {
count++;
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
System.out.println();
}
}
排序算法
介绍
排序也称排序算法(Sort Algorithm),排序是将一组数据,依指定的顺序进行排列的过程
排序的分类
- 内部排序:指将需要处理的所有数据都加载到内部存储器(内存)中进行排序。
- 外部排序:数据量过大,无法全部加载到内存中,需要借助外部存储(文件等)进行排序。
- 排序算法的分类
算法的时间复杂度
度量一个程序(算法)执行时间的两种方法
- 事后统计的方法:这种方法可行, 但是有两个问题:一是要想对设计的算法的运行性能进行评测,需要实际运行该程序;二是所 得时间的统计量依赖于计算机的硬件、软件等环境因素, 这种方式,要在同一台计算机的相同状态下运行,才能比较那个算法速度更快。
- 事前估算的方法:通过分析某个算法的时间复杂度来判断哪个算法更优.
时间频度
时间频度:一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为 T(n)。
举例说明-基本案例:
- 比如计算 1-100 所有数字之和, 我们设计两种算法:
举例说明-忽略常数项:
结论:
- 2n+20 和 2n 随着 n 变大,执行曲线无限接近, 20 可以忽略
- 3n+10 和 3n 随着 n 变大,执行曲线无限接近, 10 可以忽略
举例说明-忽略系数
结论:
- 随着 n 值变大,5n^2+7n 和 3n^2 + 2n ,执行曲线重合, 说明 这种情况下, 5 和 3 可以忽略。
- 而 n^3+5n 和 6n^3+4n ,执行曲线分离,说明多少次方式关键
时间复杂度
- 一般情况下,算法中的基本操作语句的重复执行次数是问题规模 n 的某个函数,用 T(n)表示,若有某个辅助函数 f(n),使得当 n 趋近于无穷大时,T(n) / f(n) 的极限值为不等于零的常数,则称 f(n)是 T(n)的同数量级函数。 记作 T(n)=O( f(n) ),称O( f(n) ) 为算法的渐进时间复杂度,简称时间复杂度。
- T(n) 不同,但时间复杂度可能相同。 如:T(n)=n²+7n+6 与 T(n)=3n²+2n+2 它们的 T(n) 不同,但时间复杂 度相同,都为 O(n²)。
- 计算时间复杂度的方法:
- 用常数 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²)
常见的时间复杂度
- 常数阶 O(1)
- 对数阶 O(log2n)
- 线性阶 O(n)
- 线性对数阶 O(nlog2n)
- 平方阶 O(n^2)
- 立方阶 O(n^3)
- k 次方阶 O(n^k)
- 指数阶 O(2^n)
常见的时间复杂度对应的图
说明:
- 常见的算法时间复杂度由小到大依次为:Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n2)<Ο(n^3)< Ο(n^k) < Ο(2^n) ,随着问题规模 n 的不断增大,上述时间复杂度不断增大,算法的执行效率越低
- 从图中可见,我们应该尽可能避免使用指数阶的算法
平均时间复杂度和最坏时间复杂度
- 平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下,该算法的运行时间。
- 最坏情况下的时间复杂度称最坏时间复杂度。一般讨论的时间复杂度均是最坏情况下的时间复杂度。这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的界限,这就保证了算法的运行时间不会 比最坏情况更长。
- 平均时间复杂度和最坏时间复杂度是否一致,和算法有关(如图:)
算法的空间复杂度
基本介绍
- 类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是 问题规模 n 的函数。
- 空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。有的算法需要占用的 临时工作单元数与解决问题的规模 n 有关,它随着 n 的增大而增大,当 n 较大时,将占用较多的存储单元,例 如快速排序和归并排序算法, 基数排序就属于这种情况
- 在做算法分析时,主要讨论的是时间复杂度。从用户使用体验上看,更看重的程序执行的速度。一些缓存产品 (redis, memcache)和算法(基数排序)本质就是用空间换时间
冒泡排序
基本介绍
冒泡排序(Bubble Sorting)的基本思想是:通过对待排序序列从前向后(从下标较小的元素开始),依次比较 相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒
优化:
因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在 排序过程中设置一个标志 flag 判断元素是否进行过交换。从而减少不必要的比较。(这里说的优化,可以在冒泡排 序写好后,在进行)
小结上面的图解过程:
- 一共进行数组的大小-1次大的循环
- 每一趟排序的次数在逐渐的减少
- 如果我们发现在某趟排序中,没有发生一次交换,可以提前结束冒泡排序。这个就是优化。
我们举一个具体的案例来说明冒泡法。我们将五个无序的数:3, 9, -1, 10, -2 使用冒泡排序法将其排成一个从小 到大的有序数列
代码实现
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Scanner;
public class Test4 {
public static void main(String[] args) {
// int arr[] = {3, 9, -1, 10, -2};
// System.out.println("排序前");
// System.out.println(Arrays.toString(arr));
//为了容易理解,我们把冒泡排序的演变过程,给大家展示
//测试一下冒泡排序的速度O(n^2),给80000个数据,测试
//创建要给80000个的随机的数据
int[] arr = new int[80000];
for (int i = 0; i < arr.length;i++){
arr[i] = (int) (Math.random()*80000);
}
Date datal = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date1Str = simpleDateFormat.format(datal);
System.out.println("排序前的时间是="+date1Str);
//测试冒泡排序
bubbleSort(arr);
Date data2 = new Date();
String date2Str = simpleDateFormat.format(data2);
System.out.println("排序前的时间是="+date2Str);
//System.out.println("排序后");
//System.out.println(Arrays.toString(arr));
/*
//第二趟排序,就是将第二大的数排在倒数第二位
for (int j = 0; j < arr.length - 2; j++) {
//如果前面的数比后面的数打,则交换
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
System.out.println("第二趟排序后的数据");
System.out.println(Arrays.toString(arr));
//第3趟排序,就是将第3大的数排在倒数第3位
for (int j = 0; j < arr.length - 3; j++) {
//如果前面的数比后面的数打,则交换
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
System.out.println("第三趟排序后的数据");
System.out.println(Arrays.toString(arr));
//第4趟排序,就是将第4大的数排在倒数第4位
for (int j = 0; j < arr.length - 4; j++) {
//如果前面的数比后面的数打,则交换
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
System.out.println("第四趟排序后的数据");
System.out.println(Arrays.toString(arr));
*/
}
//将前面冒泡排序算法,封装成一个方法
public static void bubbleSort(int[] arr) {
//冒泡排序的时间复杂度O(n^2)
int temp = 0;//临时变量
boolean flag = false; //标识变量,表示是否进行过交换
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1; j++) {
//如果前面的数比后面的数打,则交换
if (arr[j] > arr[j + 1]) {
flag = true;
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
//System.out.println("第" + (i + 1) + "趟排序后的数据");
//System.out.println(Arrays.toString(arr));
if (!flag) {//在一趟排序中,一次交换都没有发生过
break;
} else {
flag = false;//重置flag,进行下次判断
}
}
}
}
选择排序
基本介绍
选择式排序也属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的
基本思想
选择排序(select sorting)也是一种简单的排序方法。它的基本思想是:第一次从 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 次,得到一个按排序码从小到大排列的有序序列。
分析图:
对一个数组的选择排序再进行讲解:
选择排序应用实例:
代码实现
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Scanner;
//选择排序
public class Test2 {
public static void main(String[] args) {
//int[] arr = {101, 34, 119, 1};
//创建要给80000个的随机的数据
int[] arr = new int[80000];
for (int i = 0; i < arr.length;i++){
arr[i] = (int) (Math.random()*80000);
}
//System.out.println("排序前");
//System.out.println(Arrays.toString(arr));
Date datal = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date1Str = simpleDateFormat.format(datal);
System.out.println("排序前的时间是="+date1Str);
selectSort(arr);
Date data2 = new Date();
String date2Str = simpleDateFormat.format(data2);
System.out.println("排序前的时间是="+date2Str);
//System.out.println("排序后");
//System.out.println(Arrays.toString(arr));
}
//选择排序
public static void selectSort(int[] arr) {
//在推导的过程,我们发现了规律,因此,可以使用for来解决
//选择排序的时间复杂度0(n^2)
for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i;
int min = arr[i];
for (int j = i + 1; j < arr.length; j++) {
if (min > arr[j]) {//说明假定的最小值,并不是最小
min = arr[j];//重置min
minIndex = j;//重置minIndex
}
}
//将最小值,放在arr[0],即交换
if (minIndex != 0) {
arr[minIndex] = arr[i];
arr[i] = min;
}
//System.out.println("第"+(i+1)+"轮后~~");
//System.out.println(Arrays.toString(arr));
}
//使用逐步推导的方式来,讲解选择排序
//第一轮
//原始的数组: 101,34,119,1
//第一轮排序: 1,34,119,101
//算法 先简单--》做复杂,就是可以把一个复杂的算法,拆分成简单的问题 --》 逐步解决
//第一轮
/* int minIndex = 0;
int min = arr[0];
for (int j = 0 + 1; j < arr.length; j++) {
if (min > arr[j]) {//说明假定的最小值,并不是最小
min = arr[j];//重置min
minIndex = j;//重置minIndex
}
}
//将最小值,放在arr[0],即交换
if (minIndex != 0) {
arr[minIndex] = arr[0];
arr[0] = min;
}
System.out.println("第一轮后~~");
System.out.println(Arrays.toString(arr));
//第二轮
minIndex = 1;
min = arr[1];
for (int j = 1 + 1; j < arr.length; j++) {
if (min > arr[j]) {//说明假定的最小值,并不是最小
min = arr[j];//重置min
minIndex = j;//重置minIndex
}
}
//将最小值,放在arr[0],即交换
if (minIndex != 1) {
arr[minIndex] = arr[1];
arr[1] = min;
}
System.out.println("第二轮后~~");
System.out.println(Arrays.toString(arr));
//第三轮
minIndex = 2;
min = arr[2];
for (int j = 2 + 1; j < arr.length; j++) {
if (min > arr[j]) {//说明假定的最小值,并不是最小
min = arr[j];//重置min
minIndex = j;//重置minIndex
}
}
//将最小值,放在arr[0],即交换
if (minIndex != 2) {
arr[minIndex] = arr[2];
arr[2] = min;
}
System.out.println("第三轮后~~");
System.out.println(Arrays.toString(arr));
*/
}
}
插入排序
基本介绍
插入式排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的。
插入排序的思想
插入排序(Insertion Sorting)的基本思想是:把 n 个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有 n-1 个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。
插入排序思路图:
应用实例:
有一群小牛,考试成绩分别是101,34,119,1 请从小到大排序
代码实现:
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
public class Test1 {
public static void main(String[] args) {
//int[] arr = {101, 34, 119, 1};
//创建要给80000个的随机的数据
int[] arr = new int[80000];
for (int i = 0; i < arr.length;i++){
arr[i] = (int) (Math.random()*80000);
}
Date datal = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date1Str = simpleDateFormat.format(datal);
System.out.println("排序前的时间是="+date1Str);
insertSort(arr);
Date data2 = new Date();
String date2Str = simpleDateFormat.format(data2);
System.out.println("排序前的时间是="+date2Str);
}
//插入排序
public static void insertSort(int[] arr) {
//使用for循环来代码简化
for (int i = 1;i < arr.length;i++){
int insertVal = arr[i];
int insertIndex = i - 1;//即arr[1]的前面这个数的下标
//给insertVal找到插入的位置
//说明
//1.insertIndex >= 0 保证在给insertVal 找插入位置,不越界
//2.insertVal < arr[insertIndex] 将插入的数,还没有找到插入位置
//3.
while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
arr[insertIndex +1] = arr[insertIndex];
insertIndex--;
}
//当推出while循环时,说明插入的位置找到,insertIndex + 1
// 这里我们判断是否需要赋值
if (insertIndex + 1 != i) {
arr[insertIndex + 1] = insertVal;
}
//System.out.println("第"+i+"轮插入");
//System.out.println(Arrays.toString(arr));
}
//使用逐步推导的方式来讲解,便于理解
//第一轮{101,34,119,1}; => {34,101,119,1}
//定义待插入的数
/* int insertVal = arr[1];
int insertIndex = 1 - 1;//即arr[1]的前面这个数的下标
//给insertVal找到插入的位置
//说明
//1.insertIndex >= 0 保证在给insertVal 找插入位置,不越界
//2.insertVal < arr[insertIndex] 将插入的数,还没有找到插入位置
//3.
while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
arr[insertIndex +1] = arr[insertIndex];
insertIndex--;
}
//当推出while循环时,说明插入的位置找到,insertIndex + 1
arr[insertIndex + 1] = insertVal;
System.out.println("第一轮插入");
System.out.println(Arrays.toString(arr));
//第二轮
insertVal = arr[2];
insertIndex = 2 - 1;
while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
arr[insertIndex +1] = arr[insertIndex];
insertIndex--;
}
arr[insertIndex + 1] = insertVal;
System.out.println("第二轮插入");
System.out.println(Arrays.toString(arr));
//第三轮
insertVal = arr[3];
insertIndex = 3 - 1;
while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
arr[insertIndex +1] = arr[insertIndex];
insertIndex--;
}
arr[insertIndex + 1] = insertVal;
System.out.println("第三轮插入");
System.out.println(Arrays.toString(arr));
*/
}
}
结论: 当需要插入的数是较小的数时,后移的次数明显增多,对效率有影响.
希尔排序
基本介绍
希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。
基本思想
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止
示意图
应用实例:
代码实现:
-
交换法
import java.util.Arrays; public class Test2 { public static void main(String[] args) { int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0}; shellSort(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组,每组有个元素),步长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)); } //希尔排序的第一轮排序 //因为第一轮排序,是将10个数据分成了5组 /* for (int i = 5; i < arr.length; i++) { //遍历各组中所有的元素(共五组,每组有两个元素),步长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)); //希尔排序的第二轮排序 //因为第二轮排序,是将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)); //希尔排序的第三轮排序 //因为第三轮排序,是将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)); */ } }
-
移动法
import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; public class Test2 { public static void main(String[] args) { //int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0}; //创建要给80000个的随机的数据 int[] arr = new int[8]; for (int i = 0; i < arr.length; i++) { arr[i] = (int) (Math.random() * 80000); } Date datal = new Date(); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String date1Str = simpleDateFormat.format(datal); System.out.println("排序前的时间是=" + date1Str); shellSort2(arr); Date data2 = new Date(); String date2Str = simpleDateFormat.format(data2); System.out.println("排序前的时间是=" + date2Str); } //使用逐步推导的方式来编写希尔排序 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组,每组有个元素),步长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)); } //希尔排序的第一轮排序 //因为第一轮排序,是将10个数据分成了5组 /* for (int i = 5; i < arr.length; i++) { //遍历各组中所有的元素(共五组,每组有两个元素),步长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)); //希尔排序的第二轮排序 //因为第二轮排序,是将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)); //希尔排序的第三轮排序 //因为第三轮排序,是将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) { //从第gap个元素,逐个对其所在组进行直接插入排序 for (int i = gap; i < arr.length; i++) { int j = i; int temp = arr[j]; if (arr[j] < arr[j - gap]) { while (j - gap >= 0 && temp < arr[j - gap]) { //移动 arr[j] = arr[j - gap]; j -= gap; } //当退出while后,就给temp找到插入的位置 arr[j] = temp; } } } } }
快速排序
基本介绍
快速排序(Quicksort)是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列
基本思想-示意图
应用实例
要求: 对 [-9,78,0,23,-567,70] 进行从小到大的排序,要求使用快速排序法。【测试8w和800w】
说明[验证分析]:
1.如果取消左右递归,结果是 -9 -567 0 23 78 70
2.如果取消右递归,结果是 -567 -9 0 23 78 70
3.如果取消左递归,结果是 -9 -567 0 23 70 78
代码实现
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
public class Test4 {
public static void main(String[] args) {
// int[] arr = {-9, 78, 0, 23, -567, 70};
// quickSort(arr, 0, arr.length - 1);
// System.out.println("arr=" + Arrays.toString(arr));
//测试快排的执行速度
//创建要给80000个的随机的数据
int[] arr = new int[80000];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) (Math.random() * 80000);
}
Date datal = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date1Str = simpleDateFormat.format(datal);
System.out.println("排序前的时间是=" + date1Str);
quickSort(arr,0,arr.length-1);
Date data2 = new Date();
String date2Str = simpleDateFormat.format(data2);
System.out.println("排序前的时间是=" + date2Str);
}
public static void quickSort(int[] arr, int left, int right) {
int l = left;//左下标
int r = right;//右下标
//pivot 中轴值
int pivot = arr[(left + right) / 2];
int temp = 0;//临时变量,作为交换时使用
//while循环的目的是让比pivot值小放到左边
//比pivot值大放到右边
while (l < r) {
//在pivot的左边一直找,找到大于等于pivot值,才退出
while (arr[l] < pivot) {
l += 1;
}
//在pivot的右边一直找,找到小于等于pivot值,才退出
while (arr[r] > pivot) {
r -= 1;
}
//如果l >= r 说明pivot的左右两的值,已经按照左边全部是
//小于等于pivot值,右边全部是大于等于pivot值
if (l >= r) {
break;
}
//交换
temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
//如果交换完后,发现这个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);
}
}
}
归并排序
基本介绍
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
思想示意图
- 基本思想
说明:可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程。 - 合并相邻有序子序列:再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤
代码实现
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
public class Test1 {
public static void main(String[] args) {
// int[] arr = {8, 4, 5, 7, 1, 3, 6, 2};
// int temp[] = new int[arr.length];//归并排序需要一个额外空间
// margeSort(arr, 0, arr.length - 1, temp);
// System.out.println("归并排序后=" + Arrays.toString(arr));
//创建要给80000个的随机的数据
int[] arr = new int[80000];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) (Math.random() * 80000);
}
int temp[] = new int[arr.length];//归并排序需要一个额外空间
Date datal = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date1Str = simpleDateFormat.format(datal);
System.out.println("排序前的时间是=" + date1Str);
margeSort(arr, 0, arr.length - 1, temp);
Date data2 = new Date();
String date2Str = simpleDateFormat.format(data2);
System.out.println("排序前的时间是=" + date2Str);
}
//分加合的方法
public static void margeSort(int[] arr, int left, int right, int[] temp) {
if (left < right) {
int mid = (left + right) / 2;//中间索引
//向左递归进行分解
margeSort(arr, left, mid, temp);
//向右递归进行分解
margeSort(arr, mid + 1, right, temp);
//合并
marge(arr, left, mid, right, temp);
}
}
//合并的方法
/**
* @param arr 排序的原始数组
* @param left 左边有序序列的初始索引
* @param mid 中间索引
* @param right 右边索引
* @param temp 做中转的数组
*/
public static void marge(int[] arr, int left, int mid, int right, int[] temp) {
int i = left;//初始化i,左边有序序列的初始索引
int j = mid + 1;//初始化j,右边有序序列的初始索引
int t = 0;//指向temp数组的当前索引
//(一)
//先把左右两边(有序)的数据按照规则填充到temp数组
//直到左右两边的有序序列,有一边处理完毕为止
while (i <= mid && j <= right) {//继续
//如果左边的有序序列的当前元素,小于等于右边有序序列的当前元素
//即将左边的当前元素,填充到temp数组
//然后t++,i++
if (arr[i] <= arr[j]) {
temp[t] = arr[i];
t += 1;
i += 1;
} else {//反之,将右边有序序列的当前元素,填充到temp数组
temp[t] = arr[j];
t += 1;
j += 1;
}
}
//(二)
//把有剩余数据的一边的数据依次全部填充到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;
}
//(三)
//将temp数组的元素拷贝到arr
//注意,并不是每次都拷贝所有
t = 0;
int tempLeft = left;
//第一次合并tempLeft = 0, right = 1 // tempLeft = 2 right = 3
// tl=0,ri=3
//最后一次tempLeft = 0 right = 7
while (tempLeft <= right) {
arr[tempLeft] = temp[t];
t += 1;
tempLeft += 1;
}
}
}
基数排序
基数排序(桶排序)介绍
- 基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或 bin sort,顾 名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用
- 基数排序法是属于稳定性的排序,基数排序法的是效率高的稳定性排序法
- 基数排序(Radix Sort)是桶排序的扩展
- 基数排序是 1887 年赫尔曼·何乐礼发明的。它是这样实现的:将整数按位数切割成不同的数字,然后按每个位数分别比较基数排序是 1887 年赫尔曼·何乐礼发明的。它是这样实现的:将整数按位数切割成不同的数字,然后按每个位数分别比较
基本思想
- 将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。 这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
- 这样说明,比较难理解,下面我们看一个图文解释,理解基数排序的步骤
代码实现
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
public class Test2 {
public static void main(String[] args) {
// int arr[] = {53, 3, 542, 748, 14, 214};
// radixSort(arr);
//创建要给80000个的随机的数据
int[] arr = new int[80000];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) (Math.random() * 80000);
}
int temp[] = new int[arr.length];//归并排序需要一个额外空间
Date datal = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date1Str = simpleDateFormat.format(datal);
System.out.println("排序前的时间是=" + date1Str);
radixSort(arr);
Date data2 = new Date();
String date2Str = simpleDateFormat.format(data2);
System.out.println("排序前的时间是=" + date2Str);
}
//基数排序方法
public static void radixSort(int[] arr) {
//根据前面的推导过程,我们可以得到最终的基数排序代码
//1.得到数组中最大得到数的位数
int max = arr[0];//假设第一数就是最大值
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
//得到最大数是几位数
int maxLength = (max + "").length();
//
//第一轮(针对每个元素的个位进行排序处理)
//定义一个二维数组,表示10个桶,每个桶就是一个一维数组
//说明
//1.二维数组包含10个一维数组
//2.为了防止在放入数的时候,数量溢出,则每个一维数组(桶),大小定为arr.length
//3.很明确,基数排序是使用空间换时间的经典算法
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) {
//第(i+1)轮(针对每个元素的对应位进行排序处理)
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
arr[index++] = bucket[k][l];
}
}
//第(i+1)轮处理后,需要将每个bucketElementCounts[k] = 0!!!!
bucketElementCounts[k] = 0;
}
// System.out.println("第"+(i+1)+"轮,对个位的排序处理arr=" + Arrays.toString(arr));
}
/*
//第一轮(针对每个元素的个位进行排序处理)
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
arr[index++] = bucket[k][l];
}
}
//第一轮处理后,需要将每个bucketElementCounts[k] = 0!!!!
bucketElementCounts[k] = 0;
}
System.out.println("第一轮,对个位的排序处理arr=" + Arrays.toString(arr));
//===============================================
//第二轮(针对每个元素的十位进行排序处理)
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
arr[index++] = bucket[k][l];
}
}
//第二轮处理后,需要将每个bucketElementCounts[k] = 0!!!!
bucketElementCounts[k] = 0;
}
System.out.println("第二轮,对个位的排序处理arr=" + Arrays.toString(arr));
//第三轮(针对每个元素的百位进行排序处理)
for (int j = 0; j < arr.length; j++) {
//取出每个元素的个位的值
int digitOfElement = arr[j] / 100 % 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
arr[index++] = bucket[k][l];
}
}
}
System.out.println("第三轮,对个位的排序处理arr=" + Arrays.toString(arr));
*/
}
}
说明
- 基数排序是对传统桶排序的扩展,速度很快
- 基数排序时经典的空间换时间的方式,占用内存很大,当对海量数据排序时,容易造成OutOfMemoryError(内存不足)。
- 基数排序时稳定的。[注:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[i],且r[i]在r[i]之前,而在排序后的序列中,r[i]仍在r[j]之前, 则称这种排序算法是稳定的;否则称为不稳定的]
- 有负数的数组,我们不用基数排序来进行排序,如果要支持负数,参考:https://code.i-harness.com/zh-CN/q/e98fa9
常用排序算法总结和对比
比较图
相关术语解释
- 稳定:如果a原本在b前面,而ab,排序之后a仍然在b的前面;
- 不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面
- 内排序:所有排序操作都在内存中完成;
- 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行。
- 时间复杂度:一个算法执行所耗费的时间。
- 空间复杂度:运行完一个程序所需内存的大小。
- n:数据规模
- k:“桶”的个数
- In-place: 不占用额外内存
- Out-place:占用额外内存
查找算法
查找算法介绍
在java中,我们常用的查找有四种:
- 顺序(线性)查找
- 二分查找/折半查找
- 插值查找
- 斐波那契查找
线性查找算法
例题
有一个数列: {1,8, 10, 89, 1000, 1234} ,判断数列中是否包含此名称【顺序查找】 要求: 如果找到了,就提示找到,并给出下标值。
代码实现
public class Test2 {
public static void main(String[] args) {
int[] arr = {1, 9, 11, -1, 34, 89};//没有顺序的数组
int index = seqSearch(arr, 11);
if (index == -1) {
System.out.println("没有找到");
} else {
System.out.println("找到,下标为=" + index);
}
}
/**
* 这里我们实现的线性查找是找到一个满足条件的值,就返回
*
* @param arr
* @param value
* @return
*/
public static int seqSearch(int[] arr, int value) {
//线性查找是逐一比对,发现有相同值,就返回下标
for (int i = 0; i < arr.length; i++) {
if (arr[i] == value) {
return i;
}
}
return -1;
}
}
二分查找算法
例题
请对一个有序数组进行二分查找 {1,8, 10, 89, 1000, 1234} ,输入一个数看看该数组是否存在此数,并且求出下标,如果没有就提示"没有这个数"。
思路示意图
代码实现
说明:增加了找到所有的满足条件的元素下标:
课后思考题: {1,8, 10, 89, 1000, 1000,1234} 当一个有序数组中,有多个相同的数值时,如何将所有的数值都查找到,比如这里的1000
import java.util.ArrayList;
import java.util.List;
public class Test2 {
//注意:使用二分查找的前提是该数组是有序的
public static void main(String[] args) {
int[] arr = {1, 8, 10, 89,1000, 1234};
//int resIndex = binarySearch(arr, 0, arr.length - 1, 88);
// System.out.println("resIndex=" + resIndex);
List<Integer> resIndexList = binarySearch2(arr,0,arr.length-1,1000);
System.out.println("resIndexList=" + resIndexList);
}
//二分查找算法
/**
* @param arr 数组
* @param left 左边的索引
* @param right 右边的索引
* @param findVal 要查找的值
* @return 如果找到就返回下标,如果没有找到,就返回-1
*/
public static int binarySearch(int[] arr, int left, int right, int findVal) {
//当left>right时,说明递归整个数组,但是没有找到
if (left > right) {
return -1;
}
int mid = (left + right) / 2;
int midVal = arr[mid];
if (findVal > midVal) {
return binarySearch(arr, mid + 1, right, findVal);
} else if (findVal < midVal) {
return binarySearch(arr, left, mid - 1, findVal);
} else {
return mid;
}
}
//完成一个课后思考题
//课后思考题: {1,8, 10, 89, 1000, 1000,1234} 当一个有序数组中,
// 有多个相同的数值时,如何将所有的数值都查找到,比如这里的1000
// 思路分析
//1.找到mid索引值,不能马上返回
//2.在找到mid索引值的左边扫描,将所有满足1000的元素的下标,加入到集合ArrayList
//3.在找到mid索引值的右边扫描,将所有满足1000的元素的下标,加入到集合ArrayList
//4.将Arraylist返回
public static List<Integer> binarySearch2(int[] arr, int left, int right, int findVal) {
//当left>right时,说明递归整个数组,但是没有找到
if (left > right) {
return new ArrayList<Integer>();
}
int mid = (left + right) / 2;
int midVal = arr[mid];
if (findVal > midVal) {
return binarySearch2(arr, mid + 1, right, findVal);
} else if (findVal < midVal) {
return binarySearch2(arr, left, mid - 1, findVal);
} else {
// 思路分析
//1.找到mid索引值,不能马上返回
//2.在找到mid索引值的左边扫描,将所有满足1000的元素的下标,加入到集合ArrayList
//3.在找到mid索引值的右边扫描,将所有满足1000的元素的下标,加入到集合ArrayList
//4.将Arraylist返回
List<Integer> resIndexlist = new ArrayList<Integer>();
//向mid索引值左边扫描,将所有满足1000的元素的下标,加入到集合ArrayList
int temp = mid -1;
while (true){
if (temp < 0 || arr[temp] != findVal){//退出
break;
}
//否则,就temp放入到resIndexlist
resIndexlist.add(temp);
temp -= 1;
}
resIndexlist.add(mid);
//向mid索引值右边扫描,将所有满足1000的元素的下标,加入到集合ArrayList
temp = mid +1;
while (true){
if (temp > arr.length-1 || arr[temp] != findVal){//退出
break;
}
//否则,就temp放入到resIndexlist
resIndexlist.add(temp);
temp += 1;
}
return resIndexlist;
}
}
}
插值查找算法
-
插值查找原理介绍:
插值查找算法类似于二分查找,不同的是插值查找每次从自适应 mid 处开始查找。 -
将折半查找中的求 mid 索引的公式 , low 表示左边索引 left, high 表示右边索引 right。key 就是前面我们讲的findVal(查找值)
-
int mid = low + (high - low) * (key - arr[low]) / (arr[high] - arr[low]) ;/插值索引/
对应前面的代码公式:
int mid = left + (right – left) * (findVal – arr[left]) / (arr[right] – arr[left]) -
举例说明插值查找算法1-100的数组
应用实例
请对一个有序数组进行插值查找 {1,8, 10, 89, 1000, 1234} ,输入一个数看看该数组是否存在此数,并且求出下标,如果没有就提示"没有这个数"。
代码实现
import org.w3c.dom.html.HTMLIsIndexElement;
import java.util.Arrays;
public class Test4 {
public static void main(String[] args) {
int[] arr = new int[100];
for (int i = 0; i < 100; i++) {
arr[i] = i + 1;
}
int index = insertValueSearch(arr,0,arr.length-1,1);
//int index = binarySearch(arr, 0, arr.length, 100);
System.out.println("index=" + index);
// System.out.println(Arrays.toString(arr));
}
public static int binarySearch(int[] arr, int left, int right, int findVal) {
System.out.println("二分查找被调用~");
//当left>right时,说明递归整个数组,但是没有找到
if (left > right) {
return -1;
}
int mid = (left + right) / 2;
int midVal = arr[mid];
if (findVal > midVal) {
return binarySearch(arr, mid + 1, right, findVal);
} else if (findVal < midVal) {
return binarySearch(arr, left, mid - 1, findVal);
} else {
return mid;
}
}
//编写插值查找算法
/**
* @param arr 数组
* @param left 左边索引
* @param right 右边的索引
* @param findVal 查找值
* @return 如果找到,就返回对应下标,如果没有找到,返回-1
*/
public static int insertValueSearch(int[] arr, int left, int right, int findVal) {
System.out.println("插值查找次数~~");
//注意findVal < arr[0]和 findVal > arr[arr.length -1]必须需要
//否则我们得到的mid可能越界
if (left > right || findVal < arr[0] || findVal > arr[arr.length - 1]) {
return -1;
}
//求出mid,自适应
int mid = left + (right - left) * (findVal - arr[left]) / (arr[right] - arr[left]);
int midVal = arr[mid];
if (findVal > midVal) {//说明应该向右边递归
return insertValueSearch(arr, mid + 1, right, findVal);
} else if (findVal > midVal) {//说明应该向左边递归
return insertValueSearch(arr, left, mid - 1, findVal);
} else {
return mid;
}
}
}
注意事项
- 对于数据量较大,关键字分布比较均匀的查找表来说,采用插值查找, 速度较快.
- 关键字分布不均匀的情况下,该方法不一定比折半查找要好
斐波那契(黄金分割法)查找算法
基本介绍
-
黄金分割点是指把一条线段分割为两部分,使其中一部分与全长之比等于另一部分与这部分之比。取其前三位数字的近似值是 0.618。由于按此比例设计的造型十分美丽,因此称为黄金分割,也称为中外比。这是一个神奇的数字,会带来意向不大的效果。
-
斐波那契数列 {1, 1, 2, 3, 5, 8, 13, 21, 34, 55 } 发现斐波那契数列的两个相邻数 的比例,无限接近 黄金分割值
0.618
原理
斐波那契查找原理与前两种相似,仅仅改变了中间结点(mid)的位置,mid 不再是中间或插值得到,而是位于黄金分割点附近,即 mid=low+F(k-1)-1(F 代表斐波那契数列),如下图所示
对F(k-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
- 类似的,每一子段也可以用相同的方式分割
- 但顺序表长度 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++;
应用实例
请对一个有序数组进行斐波那契查找 {1,8, 10, 89, 1000, 1234} ,输入一个数看看该数组是否存在此数,并且求出下标,如果没有就提示"没有这个数"。
代码实现
import java.util.Arrays;
public class Test1 {
public static int maxSize = 20;
public static void main(String[] args) {
int[] arr = {1, 8, 10, 89, 1000, 1234};
System.out.println("index="+fibSearch(arr,1234));
}
//因为后面我们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 a 数组
* @param key 我们需要查找的关键值
* @return 返回对应的下标,如果没有返回-1
*/
public static int fibSearch(int[] a, int key) {
int low = 0;
int high = a.length - 1;
int k = 0;//表示斐波那契分割数组的下标
int mid = 0;//存放mid值
int f[] = fib();//获取到斐波那契数列
//获取到斐波那契分割数值的下标
while (high > f[k] - 1) {
k++;
}
//因为f[k]值可能大于a的长度,因此我们需要使用Arrays类,构造一个新的数组,并指向temp[]
//不是的部分会使用0填充
int[] temp = Arrays.copyOf(a, f[k]);
//实际上需求使用a数组最后的数填充temp
//举例:
//temp = {1,8,10,89,1000,1234,0,0,0} =>temp = {1,8,10,89,1000,1234,1234,1234,1234}
for (int i = high + 1; i < temp.length; i++) {
temp[i] = a[high];
}
//使用while来循环处理,找到我们的数key
while (low <= high) {//只要个条件满足,就可以找到
mid = low + f[k - 1] - 1;
if (key < temp[mid]){//我们应该向继续向数组的前面查找
high = mid -1;
//为什么是k--
//说明
//1.全部元素=循环的元素+后边的元素
//2.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 = f[k-1-1]-1
k--;
}else if (key > temp[mid]){//我们应该向继续向数组的后面查找
low = mid + 1;
//为什么是k--
//说明
//1.全部元素=循环的元素+后边的元素
//2.f[k]=f[k-1]+f[k-2]
//因为前面有f[k-1]个元素,所以可以继续拆分f[k-2]=f[k-3]+f[k-4]
//即在f[k-2]前面继续查找k-=2
//即下次循环mid = f[k-1-2]-1
k -=2;
} else {//找到
//需要确定,返回的是哪个下标
if (mid <= high){
return mid;
} else {
return high;
}
}
}
return -1;//没有找到
}
}
哈希表
哈希表(散列)-Google上机题
- 看一个实际需求,google 公司的一个上机题:
- 有一个公司,当有新的员工来报道时,要求将该员工的信息加入(id,性别,年龄,住址…),当输入该员工的id 时,要求查找到该员工的 所有信息.
- 要求: 不使用数据库,尽量节省内存,速度越快越好=>哈希表(散列)
基本介绍
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表
应用实例
有一个公司,当有新的员工来报道时,要求将该员工的信息加入(id,性别,年龄,名字,住址…),当输入该员工的 id 时, 要求查找到该员工的 所有信息.
要求:
- 不使用数据库,速度越快越好=>哈希表(散列)
- 添加时,保证按照 id 从低到高插入 [课后思考:如果 id 不是从低到高插入,但要求各条链表仍是从低到高,怎么解决?]
- 使用链表来实现哈希表, 该链表不带表头[即: 链表的第一个结点就存放雇员信息]
- 思路分析并画出示意图
代码实现
import javax.crypto.EncryptedPrivateKeyInfo;
import java.awt.*;
import java.util.Scanner;
import java.util.SplittableRandom;
public class Test2 {
public static void main(String[] args) {
// 创建哈希表
HashTab hashTab = new HashTab(7);
//写一个简单的菜单
String key = "";
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("add: 添加雇员");
System.out.println("list: 显示雇员");
System.out.println("find: 查找雇员");
System.out.println("exit: 退出系统");
key = scanner.next();
switch (key) {
case "add":
System.out.println("输入id");
int id = scanner.nextInt();
System.out.println("输入名字");
String name = scanner.next();
//创建雇员
Emp emp = new Emp(id, name);
hashTab.add(emp);
break;
case "list":
hashTab.list();
break;
case "find":
System.out.println("请输入要查找的id");
id = scanner.nextInt();
hashTab.findEmpById(id);
break;
case "exit":
scanner.close();
System.exit(0);
default:
break;
}
}
}
}
//创建HashTab 管理多条链表
class HashTab {
private EmpLinkedList[] empLinkedListArray;
private int size;//表示共有多少条链表
//构造器
public HashTab(int size) {
this.size = size;
//初始化empLinkedLists
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.printf("在第%d条链表中找到雇员 id=%d\n",(empLinkedListNO+1),id);
} else {
System.out.println("在哈希表中,没有找到该雇员~");
}
}
//编写散列函数,使用一个简单取模法
public int hashFun(int id) {
return id % size;
}
}
//表示一个雇员
class Emp {
public int id;
public String name;
public Emp next;//next默认为空
public Emp(int id, String name) {
super();
this.id = id;
this.name = name;
}
}
//创建EmpLinkedList,表示链表
class EmpLinkedList {
//头指针,执行第一个Emp,因此我们这个链表的head是直接指向第一个Emp
private Emp head;//默认null
//添加雇员链表
//说明
//1.假定,当添加雇员时,id是自增长,即id的分配总是从小到大
//因此我们将该雇员直接加入到本链表的最后即可
public void add(Emp emp) {
//如果是添加第一个雇员
if (head == null) {
head = emp;
return;
}
//如果不是第一个雇员,则使用一个辅助的指针,帮助定位到最后
Emp curEmp = head;
while (true) {
if (curEmp.next == null) {//说明到链表最后
break;
}
curEmp = curEmp.next;//后移
}
//退出是直接将emp加入链表
curEmp.next = emp;
}
//遍历链表的雇员信息
public void list(int no) {
if (head == null) {//说明链表为空
System.out.println("第 " + (no + 1) + " 链表为空");
return;
}
System.out.print("第 " + (no + 1) + " 链表的信息为");
Emp curEmp = head;//辅助指针
while (true) {
System.out.printf(" => id=%d name=%s\t", curEmp.id, curEmp.name);
if (curEmp.next == null) {//说明curEmp已经是最后结点
break;
}
curEmp = curEmp.next;//后移,遍历
}
System.out.println();
}
//根据id查找雇员
//如果查找到,就返回Emp,如果没有找到,就返回null
public Emp findEmpById(int id){
//判断链表是否为空
if (head == null){
System.out.println("链表为空");
return null;
}
//辅助指针
Emp curEmp = head;
while (true){
if (curEmp.id == id){//找到
break;//这时curEmp就指向要查找雇员
}
//退出
if (curEmp.next == null){//说明遍历当前链表没有找到该雇员
curEmp = null;
break;
}
curEmp = curEmp.next;//后移
}
return curEmp;
}
}
树结构的基础部分
二叉树
为什么需要树这种数据结构
-
数组存储方式的分析
优点:通过下标方式访问元素,速度快。对于有序数组,还可使用二分查找提高检索速度。
缺点:如果要检索具体某个值,或者插入值(按一定顺序)会整体移动,效率较低 [示意图]
-
链式存储方式的分析
优点:在一定程度上对数组存储方式有优化(比如:插入一个数值节点,只需要将插入节点,链接到链表中即可, 删除效率也很好)。
缺点:在进行检索时,效率仍然较低,比如(检索某个值,需要从头节点开始遍历) [示意图] -
树存储方式分析
能提高数据存储,读取的效率, 比如利用 二叉排序树(Binary Sort Tree),既可以保证数据的检索速度,同时也可以保证数据的插入,删除,修改的速度。【示意图,后面详讲】
案例: [7, 3, 10, 1, 5, 9, 12]
树示意图
树的常用术语:
- 节点
- 根节点
- 父节点
- 子节点
- 叶子结点(没有子节点的节点)
- 节点的权(节点值)
- 路径(从root节点找到该节点的路线)
- 层
- 子树
- 树的高度(最大层数)
- 森林:多棵子树构成森林