一、概述
数据结构包括:线性结构 与 非线性结构
1.1 线性结构
对于线性结构,有两种不同的存储方式:顺序存储方式 和 链式存储方式。
顺序存储存储方式 即为顺序表,顺序表中的存储元素是连续的。也就是说,在内存分配的时候,地址是连续的。比如数组,存储元素就是连续的。
链式存储方式 也称链表。如单链表、双链表。链式存储的存储元素不一定是连续的。每个数据相当于一个节点,元素节点中存放数据元素以及地址信息,通过指针相连。这样就可以充分利用碎片内存。
线性结构常见的有:数组、队列、链表、栈等。
数组的优点:
——数据存储连续,可以直接访问第N个元素。
数组的缺点:
——请求的位置如果没有存储数据,其他对象也无法使用该地址
——数组创建之后大小是固定的,所以需要创建新的数组才能进行扩容
链表的优点:
——当需要为数组分配10000个位置,但不相连,这时就无法为数组分配内存。而只要有足够的内存空间,就能为链表分配内存。
链表的缺点:
——需要读取链表的最后一个元素时,因为不知道其地址,必须遍历访问。效率较低。
1.2 非线性结构
非线性结构可能不是一对一的关系。比如一个节点下面有左节点、右节点、或者其他节点,不是一对一的关系。
非线性结构包括:二维数组、多维数组、广义表、树结构、图结构。
二、稀疏数组 和 队列
2.1 稀疏数组
1. 基本介绍
当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组。
稀疏数组的处理方法是:
1)记录数组一共有几行几列,有多少个不同的值
2)把具有不同值的元素的行列及值记录在一个小规模的数组中,从而缩小程序的规模
如果使用原始的数组进行保存,那么需要保存 6 × 7 = 42个 数据。
稀疏数组:
第一行:数组总共的行数、列数、一共几个非零值
其他行:每一个非零值的行、列、数据大小
现在只需要保存 9 × 3 = 27 个数据
2. 应用实例
二维数组 转 稀疏数组 的思路:
1、遍历原始的二维数组,得到有效数据的个数sum
2、根据sum可以创建稀疏数组 sparseArr = int [sum+1][3] - sum+1行,3列
3、将二维数组的有效数据,存入到系数数组稀疏数组 转 原始二维数组 的思路:
1、先读取系数数组的第一行,根据第一行的数据,创建原始的二维数组。比如 chessArr2 = int[11][11]
2、再读取稀疏数组后几行的数据,并赋给原始的二维数组
3. 代码实现
【1】将上面类似的二位数组棋盘保存到稀疏数组中
object SparseArray {
def main(args: Array[String]): Unit = {
//创建一个原始的二维数组 11 * 11
//0:表示没有棋子,1:表示 黑子,2:表示 白子
val chessArr1 = Array.ofDim[Int](11, 11)
chessArr1(1)(2) = 1
chessArr1(2)(3) = 2
//输出
println(s"原始的二维数组")
for (i <- 0 to chessArr1.length - 1) {
for (j <- 0 to chessArr1(i).length - 1) {
print(chessArr1(i)(j) + "\t")
}
println()
}
/*
0 0 0 0 0 0 0 0 0 0 0
0 0 1 0 0 0 0 0 0 0 0
0 0 0 0 2 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
*/
//将二维数组 转 稀疏数组
//1. 遍历二维数组,得到非0数据的个数
var sum = 0
for (i <- 0 to chessArr1.length - 1) {
for (j <- 0 to chessArr1(i).length - 1) {
if (chessArr1(i)(j) != 0) sum += 1
}
}
//2.创建对应的稀疏数组
val sparseArr = Array.ofDim[Int](sum + 1, 3)
//给稀疏数组赋值
sparseArr(0)(0) = 11 //行
sparseArr(0)(1) = 11 //列
sparseArr(0)(2) = sum //数值
//遍历二维数组,将非0的值存放到sparseArr中
var count = 1
for (i <- 0 to chessArr1.length - 1) {
for (j <- 0 to chessArr1(i).length - 1) {
if (chessArr1(i)(j) != 0) {
sparseArr(count)(0) = i
sparseArr(count)(1) = j
sparseArr(count)(2) = chessArr1(i)(j)
count += 1
}
}
}
//输出稀疏数组的形式
println(s"稀疏数组为======")
for (i <- 0 until (sparseArr.length)) {
println(s"${sparseArr(i)(0)}\t${sparseArr(i)(1)}\t${sparseArr(i)(2)}")
}
}
}
【2】稀疏数组输出的 sparsearray.text 内容如下:
【3】将稀疏数组文件中的内容恢复至传统的二维数组棋盘;
//将稀疏数组 恢复成原始的二维度数组
//1.先读取稀疏数组的第一行,根据第一行的数据,创建原始二维数组
val chessArr2 = Array.ofDim[Int](sparseArr(0)(0), sparseArr(0)(1))
//输出恢复后的二维数组
for (i <- 1 until sparseArr.length) {
val row = sparseArr(i)(0)
val col = sparseArr(i)(1)
val value = sparseArr(i)(2)
chessArr2(row)(col) = value
}
println(s"恢复后的二维数组")
for (i <- 0 to chessArr2.length - 1) {
for (j <- 0 to chessArr2(i).length - 1) {
print(chessArr2(i)(j) + "\t")
}
println()
}
2.2 队列
队列是一个有序列表,可以用数组
或是链表
来实现
遵循先进先出
的原则。先存入队列的数据,要先取出。后存入的要后取出
1. 数组模拟队列
队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如下图, 其中 maxSize 是该队列的最大容量。因为队列的输出、输入是分别从前后端来处理,因此需要两个变量 front及 rear分别记录队列前后端的下标,front 会随着数据输出而改变,而 rear则是随着数据输入而改变。
数据入队列:addQueue
1、将尾指针向后移:rear+1,当 front == rear【空】
2、若尾指针 rear 小于队列的最大下标 maxSize-1,则将数据存入 rear 所指的数组元素中,否则无法存入数据。rear == maxSize-1【队列满】
——代码实现
package Algotithm
import java.util.Scanner
object ArrayQueueDemo {
def main(args: Array[String]): Unit = {
//测试
val queue = new ArrayQueue().init(5)
var key = ' '
val scanner = new Scanner(System.in)
var loop = true
while (loop) {
println(s"s(show):显示队列")
println(s"e(exit):退出程序")
println(s"a(add):添加数据到队列")
println(s"g(get):从队列取出数据")
println(s"h(head):查看队列头的数据")
key = scanner.next.charAt(0) //接收一个字符
key match {
case 's' =>
try {
queue.showQueue()
} catch {
case exception: Exception =>
println(s"数据查询异常=>${exception}")
}
case 'e' => loop = false
case 'a' => {
println(s"输出一个整数:")
val input = scanner.nextInt()
queue.addQueue(input)
}
case 'g' => println(queue.getQueue())
case 'h' => try {
println(s"队列头数据为:${queue.headQueue}")
} catch {
case exception: Exception =>
exception.printStackTrace()
}
case _ => println("输入无效,请重新输入")
}
}
}
}
//使用数组模拟队列
class ArrayQueue {
private var maxSize: Int = _ //表示数组的最大容量
private var front: Int = _ //队列头
private var rear: Int = _ //队列尾
private var arr: Array[Int] = _ //用于存放数据,模拟队列
//创建队列的构造器
def init(maxSize: Int): ArrayQueue = {
this.maxSize = maxSize
front = -1 //指向队列头部
rear = -1 //指向队列尾部
arr = new Array[Int](this.maxSize)
this
}
//1、判断队列是否满
def isFull: Boolean = {
rear == maxSize - 1
}
//2、判断队列是否为空
def isEmpty: Boolean = {
rear == front
}
//3、添加数据到队列
def addQueue(n: Int): Unit = {
if (isFull) {
println("队列满,不能存入数据")
return
}
rear += 1 //rear后移
arr(rear) = n
}
//4、获取队列的数据,出队列
def getQueue(): Int = {
//判断队列是否为空
if (isEmpty) {
throw new Exception(s"队列为空,不能取数据")
}
front += 1
arr(front)
}
//5、显示队列的所有数据
def showQueue(): Unit = {
if (isEmpty) {
println(s"队列为空,不能取数据")
}
for (a <- front+1 to rear) {
println(s"第${a}个元素为:${arr(a)}")
}
}
//6、显示队列的头数据(不是取数据)
def headQueue: Int = {
if (isEmpty) {
throw new Exception(s"队列为空,不能取数据")
}
arr(front + 1)
}
}
2. 数组模拟环形队列
问题分析:目前数据模拟队列存在一个问题:由于没有对数据进行取模,所以目前数组只能使用一次。front指针到MaxSize的时候,虽然数组里的没有数据,但由于 front=MaxSize 仍然无法往里写入数据,数组不能重复使用。
优化:将这个数据使用算法,该进程一个环形的队列 取模:%
思路如下:
1、front 变量的含义做一个调整:front 指向队列的第一个元素。也就是说,arr[front] 就是队列的第一个元素
front的初始值 = 0
2、rear 变量的含义做一个调整:rear 指向队列的最后一个元素的后一个位置。因为希望空出一个空间做为约定。
rear的初始值 = 0
3、当队列满时,条件是:rear = maxSize-1 => (rear + 1) % maxSize = front【满】
4、当队列空时,条件是:rear = front【空】
5、队列中有效的数据的个数:(rear + maxSize - front) % maxSize // rear=1 front=0
6、在原来数组的基础上修改,得到环形队列
——代码实现
package Algotithm
import java.util.Scanner
object CircleArrayQueueDemo {
def main(args: Array[String]): Unit = {
println(s"==========测试数组模拟环形队列案例==========")
//测试
val queue = new CircleArrayQueue().init(2)
var key = ' '
val scanner = new Scanner(System.in)
var loop = true
while (loop) {
println(s"s(show):显示队列")
println(s"e(exit):退出程序")
println(s"a(add):添加数据到队列")
println(s"g(get):从队列取出数据")
println(s"h(head):查看队列头的数据")
key = scanner.next.charAt(0) //接收一个字符
key match {
case 's' =>
try {
queue.showQueue()
} catch {
case exception: Exception =>
println(s"数据查询异常=>${exception}")
}
case 'e' => loop = false
case 'a' => {
println(s"输出一个整数:")
val input = scanner.nextInt()
queue.addQueue(input)
}
case 'g' => println(queue.getQueue())
case 'h' => try {
println(s"队列头数据为:${queue.headQueue}")
} catch {
case exception: Exception =>
exception.printStackTrace()
}
case _ => println("输入无效,请重新输入")
}
}
}
}
class CircleArrayQueue {
private var maxSize: Int = _ //表示数组的最大容量
private var front: Int = _ //队列头
private var rear: Int = _ //队列尾
private var arr: Array[Int] = _ //用于存放数据,模拟队列
//创建队列的构造器
def init(maxSize: Int): CircleArrayQueue = {
this.maxSize = maxSize
front = 0 //指向队列头部
rear = 0 //指向队列尾部
arr = new Array[Int](this.maxSize)
this
}
//1、判断队列是否满
def isFull: Boolean = {
(rear + 1) % maxSize == front
}
//2、判断队列是否为空
def isEmpty: Boolean = {
rear == front
}
//3、添加数据到队列
def addQueue(n: Int): Unit = {
if (isFull) {
println("队列满,不能存入数据")
return
}
//直接将数据加入
arr(rear) = n
rear = (rear + 1) % maxSize //rear后移
}
//4、获取队列的数据,出队列
def getQueue(): Int = {
//判断队列是否为空
if (isEmpty) {
throw new Exception(s"队列为空,不能取数据")
}
//需要分析出front是指向队列的第一个元素
//1. 先把front对应的值保存到一个临时变量
//2. 将front后移,考虑取模
//3.将临时保存的变量返回
val value = arr(front)
front = (front + 1) % maxSize
value
}
//5、显示队列的所有数据
def showQueue(): Unit = {
if (isEmpty) {
throw new Exception("队列为空,不能取数据")
}
//思路:从front开始遍历,遍历多少个元素
//
for (a <- front to front + size - 1) {
println(s"front=${front} ,rear=${rear}, size=${size}, 第${a - front}个元素为:${arr(a % maxSize)}")
}
}
//求出当前队列有效数据的个数
def size: Int = {
(rear + maxSize - front) % maxSize
}
//6、显示队列的头数据(不是取数据)
def headQueue: Int = {
if (isEmpty) {
throw new Exception(s"队列为空,不能取数据")
}
arr(front)
}
}
注意:
设置数组大小为2时,队列最多只能容纳1个元素。因为空出一个空间作为约定。
三、链表
链表是一种有序的列表,但在内存中的存储如下:
小结:
1、链表是以节点的方式来存储。
2、链表的一个节点分为:data域 和 next域。next域指向的是下一个节点的地址。
3、链表的各个节点不一定是连续存储
4、链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定
3.1 单链表
1. 单链表介绍 - 逻辑结构
2. 单链表的应用实例
使用带head头节点的单向链表实现 - 水浒英雄排行榜管理
第一种方法:在添加英雄时,直接添加到链表的尾部
第二种方法:在添加英雄时,根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示)
思路:
1、创建一个 head 头节点,表示单链表的头
2、每添加一个节点,就直接加入到链表的最后
3、遍历
第一种方法:直接添加
package Algotithm
import scala.util.control.Breaks
object SingleLinkedListDemo {
def main(args: Array[String]): Unit = {
//进行测试
//先创建节点
val hero1 = new HeroNode(1, "宋江", "及时雨")
val hero2 = new HeroNode(2, "卢俊义", "玉麒麟")
val hero3 = new HeroNode(3, "吴用", "智多星")
//创建链表
val list = new SingleLinkedList
//加入
list.add(hero1)
list.add(hero2)
list.add(hero3)
//显示
list.list
}
}
//定义singleLinkedList管理英雄
class SingleLinkedList {
//先初始化一个头节点,头节点不要动,不存放具体的数据
private val head: HeroNode = new HeroNode(0, "", "")
//添加节点到单向链表
//思路:当不考虑编号顺序时
//1.找到当前链表的最后节点
//2. 将最后这个节点的next 指向 新的节点
def add(heroNode: HeroNode): Unit = {
//因为head节点不能动,因此我们需要一个辅助变量temp
var temp = head
//遍历链表,找到最后
while (temp.next != null) {
//如果没有找到最后,就将temp后移
temp = temp.next
}
//当退出while循环时,temp就指向了链表的最后
temp.next = heroNode
}
//显示链表
def list: Unit = {
//判断链表是否为空
if (head.next == null) {
println(s"链表为空")
return
}
//因为头节点不能动,因此需要一个辅助变量来遍历
var temp = head.next
while (temp != null) { //判断是否到链表最后
println(temp)
temp = temp.next
}
}
}
//定义一个HeroNode,每一个HeroNode对象就是一个节点
class HeroNode {
var no: Int = _
var name: String = _
var nickname: String = _
var next: HeroNode = _ //指向下一个节点
//构造器
def this(hNo: Int, hName: String, hNickname: String) {
this
this.no = hNo
this.name = hName
this.nickname = hNickname
}
override def toString: String = {
s"HeroNode [no=${no}, name=${name}, nickname=${nickname}]"
}
}
第二种方法:指定位置
思路:需要按照编号的顺序添加
1、首先找到新添加的节点的位置,通过辅助遍历(指针)
2、新的节点.next = temp.next
3、将temp.next = 新的节点
单链表节点的创建和遍历、顺序插入
package Algotithm
object SingleLinkedListDemo {
def main(args: Array[String]): Unit = {
//进行测试
//先创建节点
val hero1 = new HeroNode(1, "宋江", "及时雨")
val hero2 = new HeroNode(2, "卢俊义", "玉麒麟")
val hero3 = new HeroNode(3, "吴用", "智多星")
//创建链表
val list = new SingleLinkedList
//加入
list.add2(hero1)
list.add2(hero3)
list.add2(hero2)
//显示
list.list
}
}
//定义singleLinkedList管理英雄
class SingleLinkedList {
//先初始化一个头节点,头节点不要动,不存放具体的数据
private val head: HeroNode = new HeroNode(0, "", "")
//添加节点到单向链表
//思路:当不考虑编号顺序时
//1.找到当前链表的最后节点
//2. 将最后这个节点的next 指向 新的节点
def add(heroNode: HeroNode): Unit = {
//因为head节点不能动,因此我们需要一个辅助变量temp
var temp = head
//遍历链表,找到最后
while (temp.next != null) {
//如果没有找到最后,就将temp后移
temp = temp.next
}
//当退出while循环时,temp就指向了链表的最后
temp.next = heroNode
}
//第二种方法在添加英雄时,根据排名将英雄插入到指定位置
//如果有这个排名,则添加失败,并给出提示
def add2(heroNode: HeroNode): Unit = {
//因为head节点不能动,因此我们需要一个辅助变量temp
//因为是单链表,因此找的temp是位于 添加位置的前一个节点,否则插入不了
var temp = head
var flag = false //标志表示编号是否存在,默认为false
while (temp.next != null) { //移动到指针末尾
if (heroNode.no == temp.next.no) {
flag = true
println(s"当前编号${temp.no}已存在,插入失败")
return
}
if(temp.next.no > heroNode.no){
val temp2 = temp.next
temp.next = heroNode
heroNode.next = temp2
return
}
temp = temp.next
}
temp.next = heroNode
}
//显示链表
def list: Unit = {
//判断链表是否为空
if (head.next == null) {
println(s"链表为空")
return
}
//因为头节点不能动,因此需要一个辅助变量来遍历
var temp = head.next
while (temp != null) { //判断是否到链表最后
println(temp)
temp = temp.next
}
}
}
//定义一个HeroNode,每一个HeroNode对象就是一个节点
class HeroNode {
var no: Int = _
var name: String = _
var nickname: String = _
var next: HeroNode = _ //指向下一个节点
//构造器
def this(hNo: Int, hName: String, hNickname: String) {
this
this.no = hNo
this.name = hName
this.nickname = hNickname
}
override def toString: String = {
s"HeroNode [no=${no}, name=${name}, nickname=${nickname}]"
}
}
单链表节点的修改
//修改节点信息,根据no编号来修改。即no编号不能修改
//说明
//1. 根据 newHeroNode 的 no 来修改即可
def update(heroNode: HeroNode): Unit = {
if (head.next == null) {
println(s"链表为空")
return
}
var temp = head
while (temp.next != null) {
if (temp.no == heroNode.no){
temp.name = heroNode.name
temp.nickname = heroNode.nickname
return
}
temp = temp.next
}
}
object SingleLinkedListDemo {
def main(args: Array[String]): Unit = {
//进行测试
//先创建节点
val hero1 = new HeroNode(1, "宋江", "及时雨")
val hero2 = new HeroNode(2, "卢俊义", "玉麒麟")
val hero3 = new HeroNode(3, "吴用", "智多星")
//创建链表
val list = new SingleLinkedList
//加入
list.add2(hero1)
list.add2(hero3)
list.add2(hero2)
//显示
list.list
//测试修改节点
val hero4 = new HeroNode(2, "卢卢", "玉麒麟。。")
list.update(hero4)
println("=====测试修改节点======")
list.list
}
}
单链表节点的删除
思路:
1、先找到需要删除的节点的前一个节点temp
2、temp.next = temp.next.next
3、被删除的节点,将不会有其他引用指向,会被垃圾回收机制回收
package Algotithm
object SingleLinkedListDemo {
def main(args: Array[String]): Unit = {
//进行测试
//先创建节点
val hero1 = new HeroNode(1, "宋江", "及时雨")
val hero2 = new HeroNode(2, "卢俊义", "玉麒麟")
val hero3 = new HeroNode(3, "吴用", "智多星")
//创建链表
val list = new SingleLinkedList
//加入
list.add2(hero1)
list.add2(hero3)
list.add2(hero2)
//显示
list.list
//测试修改节点
//val hero4 = new HeroNode(2, "卢卢", "玉麒麟。。")
//list.update(hero4)
//println("=====测试修改节点======")
//list.list
//删除一个节点
println("=====测试删除节点======")
list.del(3)
list.list
}
}
//定义singleLinkedList管理英雄
class SingleLinkedList {
//先初始化一个头节点,头节点不要动,不存放具体的数据
private val head: HeroNode = new HeroNode(0, "", "")
//添加节点到单向链表
//思路:当不考虑编号顺序时
//1.找到当前链表的最后节点
//2. 将最后这个节点的next 指向 新的节点
def add(heroNode: HeroNode): Unit = {
//因为head节点不能动,因此我们需要一个辅助变量temp
var temp = head
//遍历链表,找到最后
while (temp.next != null) {
//如果没有找到最后,就将temp后移
temp = temp.next
}
//当退出while循环时,temp就指向了链表的最后
temp.next = heroNode
}
//第二种方法在添加英雄时,根据排名将英雄插入到指定位置
//如果有这个排名,则添加失败,并给出提示
def add2(heroNode: HeroNode): Unit = {
//因为head节点不能动,因此我们需要一个辅助变量temp
//因为是单链表,因此找的temp是位于 添加位置的前一个节点,否则插入不了
var temp = head
var flag = false //标志表示编号是否存在,默认为false
while (temp.next != null) { //移动到指针末尾
if (heroNode.no == temp.next.no) {
flag = true
println(s"当前编号${temp.no}已存在,插入失败")
return
}
if (temp.next.no > heroNode.no) {
val temp2 = temp.next
temp.next = heroNode
heroNode.next = temp2
return
}
temp = temp.next
}
temp.next = heroNode
}
//修改节点信息,根据no编号来修改。即no编号不能修改
//说明
//1. 根据 newHeroNode 的 no 来修改即可
def update(heroNode: HeroNode): Unit = {
if (head.next == null) {
println(s"链表为空")
return
}
var temp = head
while (temp.next != null) {
if (temp.no == heroNode.no) {
temp.name = heroNode.name
temp.nickname = heroNode.nickname
return
}
temp = temp.next
}
}
//删除节点
//思路
//1. head不能动,因此需要一个temp辅助节点,找到待删除节点的前一个节点
//2. 在比较时,是temp.next.no 和 需要删除的节点的no比较
def del(no: Int): Unit = {
var temp = head
while (temp.next != null) {
if (temp.next.no == no) {
if (temp.next.next != null) {
temp.next = temp.next.next
return
} else {
temp.next = null
return
}
}
temp = temp.next
}
}
//显示链表
def list: Unit = {
//判断链表是否为空
if (head.next == null) {
println(s"链表为空")
return
}
//因为头节点不能动,因此需要一个辅助变量来遍历
var temp = head.next
while (temp != null) { //判断是否到链表最后
println(temp)
temp = temp.next
}
}
}
//定义一个HeroNode,每一个HeroNode对象就是一个节点
class HeroNode {
var no: Int = _
var name: String = _
var nickname: String = _
var next: HeroNode = _ //指向下一个节点
//构造器
def this(hNo: Int, hName: String, hNickname: String) {
this
this.no = hNo
this.name = hName
this.nickname = hNickname
}
override def toString: String = {
s"HeroNode [no=${no}, name=${name}, nickname=${nickname}]"
}
}
3. 单链表面试题
1> 求单链表中节点的个数
def getLength(head: HeroNode): Int = {
var num = 0
var temp = head
while (temp.next != null) {
num += 1
temp = temp.next
}
num
}
2> 查找单链表中的倒数第k个节点
思路:
1、编写一个方法,接受head节点,同时接受一个index
2、index表示的是倒数第index个节点
3、先把链表从头到尾遍历,得到链表的总的长度 getLength
4、得到size后,从链表的第一个开始遍历(size-index)个,就可以得到
//2.查找单链表中的倒数第k个节点
def findLastNode(head: HeroNode, index: Int): HeroNode = {
if (head.next == null) return null
val list = new SingleLinkedList
val listLength = list.getLength(head) //得到当前链表的长度
var temp = head.next
for (i <- 0 until listLength - index) {
temp = temp.next
}
temp
}
3> 单链表的反转
思路:
1、先定义一个节点 reverseHea = new HeroNode()
2、从头到尾遍历原来的链表,每遍历一个节点,就将其取出,放在新的链表的reverseHead的最前端。
3、原来的链表的head.next = reverseHead.next
def reversetList(head: HeroNode): Unit = {
//如果当前链表为空,或只有一个节点,无需反转,直接返回
if (head.next == null || head.next.next == null) return
var cur: HeroNode = head.next
var next: HeroNode = null //指向当前节点的下一个节点
val reverseHead = new HeroNode(0, "", "")
//遍历原来的链表
//每遍历一个节点,就将其取出,并放在新的链表的最前端
while (head.next != null) {
cur = head.next
head.next = cur.next
next = reverseHead.next
reverseHead.next = cur
cur.next = next
}
head.next = reverseHead.next
}
4> 从尾到头打印单链表【方式1:反向遍历。方式2:stack栈】
思路:
1、逆序打印单链表
方式一:先将单链表反转,然后再遍历。这样做的问题是,会破坏原来单链表的结构,不建议
方式二:利用栈的数据结构,将各个节点压入到栈中,然后利用栈的先进后出的特点,就实现了逆序打印的效果
//4.链表反转并打印
//没有改变链表本身的结构
def reversePrint(heroNode: HeroNode): Unit = {
if (heroNode.next == null) {
return
}
//创建Stack栈
val stack = new mutable.Stack[HeroNode]()
var temp = heroNode.next
//将链表的所有节点压入栈
while (temp != null) {
stack.push(temp)
temp = temp.next
}
while (!stack.isEmpty) println(stack.pop())
}
5> 合并两个有序的单链表,合并之后的链表依然有序
3.2 双向链表
单向链表的缺点:
1)单向链表查找的方向只能是一个方向,而双向链表可以向前或者向后查找
2)单向链表不能自我删除,需要靠辅助接点。而双向链表可以自我删除。
双向链表的遍历、添加、修改、删除操作:
1)遍历:和单链表一样,可以向前,也可以向后查找
2)添加(默认添加到双向链表的最后)
(1)先找到双向链表的最后节点
(2)temp.next = nextHeroNode
(3)newHeroNode.pre = temp
3)修改:和单向链表一样
4)删除:
(1)因为是双向链表,因此可以实现自我删除某个节点
(2)直接找到要删除的这个节点。如temp
(3)temp.pre.next = temp.next
(4)temp.next.pre = temp.pre
package Algotithm
object DoubleLinkedListDemo {
def main(args: Array[String]): Unit = {
//测试
println(s"双向链表的测试")
//先创建节点
val hero1 = new HeroNode2(1, "宋江", "及时雨")
val hero2 = new HeroNode2(2, "卢俊义", "玉麒麟")
val hero3 = new HeroNode2(3, "吴用", "智多星")
//创建链表
val list = new DoubleLinkedList
//加入
list.add(hero1)
list.add(hero2)
list.add(hero3)
list.list
//修改
val newNode = new HeroNode2(3,"林冲","豹子头")
list.update(newNode)
println("修改后的链表情况>>>>>")
list.list
//删除
list.del(3)
println("删除后的链表情况>>>>>")
list.list
}
}
//创建一个双向链表的类
class DoubleLinkedList {
//初始化头节点
private val head = new HeroNode2(0, "", "")
//返回头节点
def getHead: HeroNode2 = head
//1. 遍历双向链表
def list: Unit = {
//判断链表是否为空
if (head.next == null) {
println(s"链表为空")
return
}
//因为头节点不能动,因此需要一个辅助变量来遍历
var temp = head.next
while (temp != null) { //判断是否到链表最后
println(temp)
temp = temp.next
}
}
//2. 添加一个节点到双向链表的最后
def add(heroNode: HeroNode2): Unit = {
//因为head节点不能动,因此我们需要一个辅助变量temp
var temp = head
//遍历链表,找到最后
while (temp.next != null) {
//如果没有找到最后,就将temp后移
temp = temp.next
}
//当退出while循环时,temp就指向了链表的最后
//形成一个双向链表
temp.next = heroNode
heroNode.pre = temp
}
//3. 修改一个节点的内容
//双向链表的节点内容修改和单向链表一样
def update(heroNode: HeroNode2): Unit = {
if (head.next == null) {
println(s"链表为空")
return
}
var temp = head
while (temp.next != null) {
if (temp.no == heroNode.no) {
temp.name = heroNode.name
temp.nickname = heroNode.nickname
return
}
temp = temp.next
}
}
//4. 从双线链表中删除节点
//说明
//1)对于双向链表,我们可以直接找到要删除的节点
//2)找到后自我删除即可
def del(no: Int): Unit = {
//判断当前链表是否为空
if (head.next == null) {
println(s"链表为空,不能删除")
return
}
var temp = head.next //辅助节点(指针)
while (temp != null) {
if (temp.no == no) {
temp.pre.next = temp.next
//如果最后一个节点,就不需要执行这句话,否则出现空指针
if (temp.next != null) {
temp.next.pre = temp.pre
}
}
temp = temp.next
}
}
}
//定义一个HeroNode,每一个HeroNode对象就是一个节点
class HeroNode2 {
var no: Int = _
var name: String = _
var nickname: String = _
var next: HeroNode2 = _ //指向下一个节点,默认null
var pre: HeroNode2 = _ //指向前一个节点,默认null
//构造器
def this(hNo: Int, hName: String, hNickname: String) {
this
this.no = hNo
this.name = hName
this.nickname = hNickname
}
override def toString: String = {
s"HeroNode2 [no=${no}, name=${name}, nickname=${nickname}]"
}
}
双向链表的第二种添加方式,按照编号顺序
package Algotithm
object DoubleLinkedListDemo {
def main(args: Array[String]): Unit = {
//测试
println(s"双向链表的测试")
//先创建节点
val hero1 = new HeroNode2(1, "宋江", "及时雨")
val hero2 = new HeroNode2(2, "卢俊义", "玉麒麟")
val hero3 = new HeroNode2(3, "吴用", "智多星")
//创建链表
val list = new DoubleLinkedList
//加入
list.add2(hero1)
list.add2(hero2)
list.add2(hero3)
list.list
}
}
//创建一个双向链表的类
class DoubleLinkedList {
//初始化头节点
private val head = new HeroNode2(0, "", "")
//返回头节点
def getHead: HeroNode2 = head
//双向链表的第二种添加方式,按照编号顺序
def add2(node: HeroNode2): Unit = {
if (head.next == null) {
head.next = node
node.pre = head
return
}
var temp = head.next
while (temp.next != null) { //移动到指针末尾
if (node.no == temp.no) {
println(s"当前编号${temp.no}已存在,插入失败")
return
}
if (temp.no > node.no) {
temp.pre.next = node
node.pre = temp.pre
temp.next.pre = node
node.next = temp.next
return
}
temp = temp.next
}
temp.next = node
}
}
3.3 单向环形链表
Josephu(约瑟夫、约瑟夫环)问题
问题描述:设编号为1,2,… n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m 的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
创建环形链表并显示
思路:
1、先创建第一个节点,让 first 指向该节点,并形成环状
2、后面每创建一个节点,就把该节点加入到已有的环形链表中即可。遍历环形链表:
1、先让一个辅助指针(变量)指向first节点
2、然后通过一个 while 循环遍历该环形链表即可
3、当 cur.next==first 表示遍历结束
package Algotithm
object Josephu {
def main(args: Array[String]): Unit = {
val list = new CircleSingleLinkedList
list.addBoy(3)
list.showBoy()
}
}
//创建一个环形的单向链表
class CircleSingleLinkedList {
//创建一个first节点,当前没有编号
private var first: Boy = _
//添加小孩节点,构建一个环形链表
def addBoy(nums: Int): Unit = {
//num 进行一个数据校验
if (nums < 1) {
println(s"nums的值不正确")
return
}
var curBoy: Boy = null //辅助指针,帮助构建环形链表
//使用for循环创建环形链表
for (i <- 1 to nums) {
//根据编号创建小孩节点
val boy = new Boy(i)
//如果是第一个小孩
if (i == 1) {
first = boy
first.setNext(first) //构成环
curBoy = first //让curBoy指向第一个小孩
} else {
curBoy.setNext(boy)
boy.setNext(first)
curBoy = boy
}
}
}
//遍历当前的环形链表
def showBoy(): Unit = {
//判断链表是否为空
if (first == null) {
println(s"链表为空")
return
}
//first 不能动,因此仍使用辅助指针完成遍历
var curBoy = first
while (true) {
println(s"小孩的编号:${curBoy.getNo}")
if (curBoy.getNext == first) { //遍历完毕
return
}
curBoy = curBoy.getNext //curBoy后移
}
}
}
//创建一个boy类,表示一个节点
class Boy {
private var no: Int = _ //编号
private var next: Boy = _ //指向下一个节点,默认null
def this(no: Int) {
this
this.no = no
}
def getNo: Int = this.no
def getNext: Boy = this.next
def setNo(no: Int): Unit = {
this.no = no
}
def setNext(next: Boy): Unit = {
this.next = next
}
}
根据用户的输入,生成出圈的顺序
思路:
1、创建一个辅助指针(变量)helper,事先指向环形链表的最后节点
补充:报数前,先让 first 和 helper 移动 k-1 次
2、当小孩报数时,让 first 和 helper 指针同时移动 m-1 次
3、将 first 指向的小孩节点出圈
first = first.next
helper.next = first
原来 first 指向的节点就没有任何引用,就会被回收package Algotithm
package Algotithm
object Josephu {
def main(args: Array[String]): Unit = {
val list = new CircleSingleLinkedList
list.addBoy(5)
list.showBoy()
//测试
list.countBoy(1, 2, 5)
}
}
//创建一个环形的单向链表
class CircleSingleLinkedList {
//创建一个first节点,当前没有编号
private var first: Boy = _
//根据用户的输入,计算出小孩出圈的顺序
/**
*
* @param startNo 表示从第几个小孩开始数数
* @param countNum 表示数几下
* @param nums 表示最初有多少个小孩在圈中
*/
def countBoy(startNo: Int, countNum: Int, nums: Int): Unit = {
//先对数据进行校验
if (first == nums || startNo < 1 || startNo > nums) {
println(s"参数输入有误,请重新输入")
return
}
//创建辅助指针,帮助完成小孩出圈
var helper = first
var flag = true
while (flag) {
if (helper.getNext == first) { //helper指向最后小孩节点
flag = false
} else
helper = helper.getNext
}
//小孩报数前,先让 first 和 helper 移动 k-1 次
for (i <- 1 until startNo) {
first = first.getNext
helper = helper.getNext
}
//当小孩报数时,让 first 和 helper 指针同时移动 m-1 次
//循环操作,直到圈中只有一个节点
var flag2 = true
while (flag2) {
if (first == helper) flag2 = false //圈中只有一个节点
else {
//让first 和 helper 同时移动 m-1 次
for (i <- 1 until countNum) {
first = first.getNext
helper = helper.getNext
}
//这是first指向的节点,就是要出圈的小孩节点
println(s"小孩出圈,${first.getNo}")
//将first指向的小孩节点出圈
first = first.getNext
helper.setNext(first)
}
}
println(s"最后留在圈中的小孩编号:${first.getNo}")
}
//遍历当前的环形链表
def showBoy(): Unit = {
//判断链表是否为空
if (first == null) {
println(s"链表为空")
return
}
//first 不能动,因此仍使用辅助指针完成遍历
var curBoy = first
while (true) {
println(s"小孩的编号:${curBoy.getNo}")
if (curBoy.getNext == first) { //遍历完毕
return
}
curBoy = curBoy.getNext //curBoy后移
}
}
}
四、栈
4.1栈的介绍
1)栈的英文为(stack)
2)栈是一个先入后出(FILO-First In Last Out)的有序列表。
3)栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端,为变化的一端,称为栈顶(Top)
,另一端为固定的一端,称为栈底(Bottom)
。
4)根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除
4.2 栈的应用场景
1)子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。
2)处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆栈中。
3)表达式的转换[中缀表达式转后缀表达式]
与求值(实际解决)。
4)二叉树的遍历。
5)图形的深度优先(depth一first)搜索法。
4.3 数组模拟栈
出栈、入栈、遍历
思路:
1、使用数组来模拟栈
2、定义一个 top 来模拟栈顶,初始化为 -1
3、入栈的操作。当有数据加入到栈时,top++;stacl[top] = data
4、出栈的操作。int value = stack[top];top–; return value
package Algotithm
object ArrayStackDemo {
def main(args: Array[String]): Unit = {
val stack = new ArrayStack(4)
stack.push(1)
stack.push(2)
stack.push(3)
stack.list
}
}
//定义一个ArrayStack表示栈
class ArrayStack {
private var maxSize: Int = _
private var stack: Array[Int] = _
private var top = -1
//构造器
def this(maxSize: Int) {
this
this.maxSize = maxSize
stack = new Array[Int](this.maxSize)
}
//栈满
def isFull: Boolean = {
top == maxSize - 1
}
//栈空
def isEmpty: Boolean = {
top == -1
}
//入栈-push
def push(value: Int): Unit = {
if (isFull) {
println(s"栈满")
return
}
top = top + 1
stack(top) = value
}
//出栈-pop
def pop(): Int = {
if (isEmpty) {
throw new RuntimeException(s"栈空,没有数据")
}
val value = stack(top)
top = top - 1
value
}
//显示栈的情况【遍历栈】
def list: Unit = {
if (isEmpty) {
println(s"栈空,没有数据")
return
}
//需要从栈顶开始显示
for (i <- 0 to top) {
println(s"stack=${stack(top - i)}")
}
}
}
4.3 链表模拟栈
4.4 栈实现综合计算器
思路:
1、通过一个 Index 值(索引)来遍历表达式
2、如果发现是一个数字,就直接入数栈
3、如果发现是一个符号,九分如下情况
3.1、如果发现当前符号栈是空,就直接入栈
3.2、如果符号栈有操作符,就进行比较。如果当前的操作符的优先级小于或者等于栈中的操作符
,就需要从数栈中pop出两个数,再从符号栈中pop出一个符号,进行运算,将得到结果入数栈,然后将当前的操作符入符号栈。如果当前的操作符优先级大于栈中的操作符,就直接入符号栈
4、表达式扫描完毕,就顺序的从数栈和符号栈中pop出相应的数和符号,并运算
5、最后在数栈中只有一个数字,就是表达式的结果