第十九章 Scala語言的數據結構和算法19.1 數據結構(算法)的介紹19.2 看幾個實際編程中遇到的問題19.2.1 一個五子棋程序19.2.2 約瑟夫問題(丟手帕問題)19.2.3 其它常見算法問題19.3 稀疏數組 sparsearray19.3.1 基本介紹19.3.2 應用實例19.3.3 課后練習19.4 隊列 queue19.4.1 隊列的一個使用場景19.4.2 隊列介紹19.4.3 數組模擬單向隊列19.4.4 數組模擬環形隊列19.5 鏈表 linked list19.5.1 鏈表介紹19.5.2 單向鏈表的介紹19.5.3 單向鏈表的應用實例19.5.4 雙向鏈表的應用實例19.5.5 單向環形鏈表的應用場景19.6 棧 stack19.6.1 看一個實際需求19.6.2 棧的介紹19.6.3 棧的幾個經典的應用場景19.6.4 棧的快速入門19.6.5 棧實現綜合計算器19.7 遞歸 recursive19.7.1 看個實際應用場景19.7.2 遞歸的概念19.7.3 遞歸快速入門19.7.4 遞歸用於解決什么樣的問題19.7.5 遞歸需要遵守的重要原則19.7.6 舉一個比較綜合的案例-迷宮問題19.8 排序 sort19.8.1 排序的介紹19.8.2 冒泡排序19.8.3 選擇排序19.8.4 插入排序19.8.5 快速排序19.8.6 歸並排序19.9 查找18.9.1 介紹19.9.2 線性查找19.9.3 二分查找19.10 哈希表(散列表)19.10.1 看一個實際需求19.10.2 哈希表的基本介紹19.10.3 應用實例19.11 二叉樹19.11.1 為什么需要樹這種數據結構19.11.2 二叉樹的示意圖19.11.3 二叉樹的概念19.11.4 二叉樹遍歷的說明19.11.5 二叉樹遍歷應用實例(前序、中序、后序)19.11.6 二叉樹-查找指定節點19.11.7 二叉樹-刪除節點19.12 順序存儲的二叉樹19.12.1 順序存儲二叉樹的概念19.12.2 順序存儲二叉樹的遍歷19.13 二叉排序樹19.13.1 先看一個需求19.13.2 二叉排序樹的介紹19.13.3 二叉排序樹的創建和遍歷19.13.4 二叉排序樹的刪除19.14 其它二叉樹
第十九章 Scala語言的數據結構和算法
19.1 數據結構(算法)的介紹
數據結構的介紹
1、數據結構是一門研究算法的學科,只從有了編程語言也就有了數據結構。學好數據結構可以編寫出更加漂亮、更加有效率的代碼。
2、要學習好數據結構就要多多考慮如何將生活中遇到的問題,用程序去實現解決。
3、程序 = 數據結構 + 算法
數據結構和算法的關系
1、算法是程序的靈魂,為什么有些網站能夠在高並發,和海量吞吐情況下依然堅如磐石,大家可能會說: 網站使用了服務器群集技術、數據庫讀寫分離和緩存技術(比如 Redis 等),那如果我再深入的問一句,這些優化技術又是怎樣被那些天才的技術高手設計出來的呢?
2、大家請思考一個問題,是什么讓不同的人寫出的代碼從功能看是一樣的,但從效率上卻有天壤之別, 拿在公司工作的實際經歷來說, 我是做服務器的,環境是 UNIX,功能是要支持上千萬人同時在線,並保證數據傳輸的穩定。在服務器上線前,做內測,一切 OK,可上線后,服務器就支撐不住了。公司的 CTO 對我的代碼進行優化,再次上線,堅如磐石。那一瞬間,我認識到程序是有靈魂的,就是算法。如果你不想永遠都是代碼工人,那就花時間來研究下算法吧!
3、本章着重講解算法的基石-數據結構。
19.2 看幾個實際編程中遇到的問題
試寫出用單鏈表表示的字符串類及字符串結點類的定義,並依次實現它的構造函數、以及計算串長度、串賦值、判斷兩串相等、求子串、兩串連接、求子串在串中位置等7個成員函數。
示例代碼如下:
def main(args: Array[String]): Unit = {
val str = "scala,scala,hello,world!"
val newStr = str.replaceAll("scala", "尚硅谷")
println("newStr=" + newStr)
}
即:自定義 replaceAll 函數。
19.2.1 一個五子棋程序
如何判斷游戲的輸贏,並可以完成存盤退出和繼續上局的功能。
19.2.2 約瑟夫問題(丟手帕問題)
Josephu 問題為:設編號為 1,2,…, n 的 n 個人圍坐一圈,約定編號為 k(1<=k<=n)的人從1開始報數,數到 m 的那個人出列,它的下一位又從1開始報數,數到 m 的那個人又出列,依次類推,直到所有人出列為止,由此產生一個出隊編號的序列。
提示:用一個不帶頭結點的循環鏈表來處理 Josephu
問題:先構成一個有 n 個結點的單循環鏈表,然后由 k 結點起從 1 開始計數,計到 m 時,對應結點從鏈表中刪除,然后再從被刪除結點的下一個結點又從 1 開始計數,直到最后一個結點從鏈表中刪除算法結束。
19.2.3 其它常見算法問題
郵差問題、最短路徑問題、漢諾塔、八皇后問題
19.3 稀疏數組 sparsearray
先看一個實際的需求
19.3.1 基本介紹
當一個數組中大部分元素為 0,或者為同一個值的數組時,可以使用稀疏數組來保存該數組。
稀疏數組的處理方法是:
1、記錄數組一共有幾行幾列,有多少個不同的值。
2、把具有不同值的元素的行列及值記錄在一個小規模的數組中,從而縮小程序的規模。
稀疏數組舉例說明
19.3.2 應用實例
1、使用稀疏數組,來保留類似前面的二維數組(棋盤、地圖等等)。
2、把稀疏數組存盤,並且可以重新恢復原來的二維數組數。
3、整體思路分析如下。
示例代碼如下:
package com.atguigu.chapter19.sparsearray
import scala.collection.mutable.ArrayBuffer
object SparseArrayDemo01 {
def main(args: Array[String]): Unit = {
// 演示一個稀疏數組的使用
val rowSize = 11
val colSize = 11
val chessMap = Array.ofDim[Int](rowSize, colSize)
// 初始化棋盤地圖
chessMap(1)(2) = 1 // 1 表示黑子
chessMap(2)(3) = 2 // 2 表示白子
println("----------原始的棋盤地圖-------------------")
// 輸出原始的棋盤地圖
for (item1 <- chessMap) { // 行:是一個一維數組
for (item2 <- item1) { // 列
printf("%d\t", item2)
}
println()
}
// 將 chessMap 轉成 稀疏數組
// 思路:效果是達到對數據的壓縮
// class Node(row, col, value) => ArrayBuffer
val sparseArray = new ArrayBuffer[Node]()
val node = new Node(rowSize, colSize, 0)
sparseArray.append(node)
for (i <- 0 until chessMap.length) {
for (j <- 0 until chessMap(i).length) {
// 判斷該值是否為0,如果不為0,就保存
if (chessMap(i)(j) != 0) {
// 就構建一個 Node
val node = new Node(i, j, chessMap(i)(j))
sparseArray.append(node) // 加入到稀疏數組
}
}
}
println("----------輸出稀疏數組---------------------")
// 輸出稀疏數組
for (node <- sparseArray) {
printf("%d\t%d\t%d\n", node.row, node.col, node.value)
}
// 存盤
// 讀盤
// 從稀疏數組恢復原始數據
// 1、讀取稀疏數組的第一個節點
val newNode = sparseArray(0)
val newRowSize = newNode.row
val newColSize = newNode.col
val chessMap2 = Array.ofDim[Int](newRowSize, newColSize)
// 遍歷稀疏數組
for (i <- 1 until sparseArray.length) {
val node = sparseArray(i)
chessMap2(node.row)(node.col) = node.value
}
println("----------從稀疏數組恢復的棋盤地圖----------")
// 輸出原始的棋盤地圖
for (item1 <- chessMap2) { // 行:是一個一維數組
for (item2 <- item1) { // 列
printf("%d\t", item2)
}
println()
}
}
}
class Node(val row: Int, val col: Int, val value: Int)
輸出結果如下:
----------原始的棋盤地圖-------------------
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 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 0
----------輸出稀疏數組---------------------
11 11 0
1 2 1
2 3 2
----------從稀疏數組恢復的棋盤地圖----------
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 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 0
19.3.3 課后練習
要求:
1、在前面的基礎上,將稀疏數組保存到磁盤上,比如 map.data。
2、恢復原來的數組時,讀取 map.data 進行恢復。
19.4 隊列 queue
19.4.1 隊列的一個使用場景
銀行排隊的案例:
19.4.2 隊列介紹
1、隊列是一個有序列表,可以用數組或是鏈表來實現。
2、遵循先入先出的原則。即:先存入隊列的數據,要先取出。后存入的要后取出。
19.4.3 數組模擬單向隊列
1、隊列本身是有序列表,若使用數組的結構來存儲隊列的數據,則隊列數組的聲明如下:其中 maxSize 是該隊列的最大容量。
2、因為隊列的輸出、輸入是分別從前后端來處理,因此需要兩個變量 front(或head) 及 rear(或tail) 分別記錄隊列前后端的下標,front 會隨着數據輸出而改變,而 rear 則是隨着數據輸入而改變,如圖所示:
圖解說明:
當我們將數據存入隊列時稱為“addqueue”,addqueue 的處理需要有兩個步驟:
1、將尾指針往后移:rear + 1,如果 front == rear [表示隊列為空]
2、若尾指引 rear 小於等於隊列的最大下標 maxSize - 1,則將數據存入 rear 所指的數組元素中,否則無法存入數據。 rear == maxSize - 1 [表示隊列已滿]
代碼實現:
package com.atguigu.chapter19.queue
import scala.io.StdIn
/**
* 1、數組模擬單向隊列
*/
object ArrayQueueDemo01 {
def main(args: Array[String]): Unit = {
// 初始化一個隊列
val queue = new ArrayQueue(3)
var key = ""
while (true) {
println("show:表示顯示隊列")
println("exit:表示退出隊列")
println("add:表示添加數據到隊列")
println("get:表示取出隊列的數據")
println("head: 查看隊列頭的數據(不改變隊列)")
key = StdIn.readLine()
key match {
case "show" => queue.showQueue()
case "exit" => System.exit(0)
case "add" => {
println("請輸入一個數據(Int類型):")
val n = StdIn.readInt()
queue.addQueue(n)
}
case "get" => {
val res = queue.getQueue()
if (res.isInstanceOf[Exception]) {
println(res.asInstanceOf[Exception].getMessage)
} else {
println(s"取出對列的數據是 $res")
}
}
case "head" => {
val res = queue.headQueue()
if(res.isInstanceOf[Exception]) {
// 顯示錯誤信息
println(res.asInstanceOf[Exception].getMessage)
}else {
println("隊列頭元素的值為=" + res)
}
}
}
}
}
}
// 使用數組模擬單向隊列
class ArrayQueue(arrMaxSize: Int) {
// 該隊列的最大容量
val maxSize = arrMaxSize
// 該數組存放數據,模擬隊列
val arr = new Array[Int](maxSize)
// 記錄隊列前端
var front = -1 // front 是隊列最前元素的索引[不含]
// 記錄隊列后端
var rear = -1 // rear 是隊列最后元素的索引[含]
// 判斷隊列是否已滿
def isFull(): Boolean = {
rear == maxSize - 1
}
// 判斷隊列是否為空
def isEmpty(): Boolean = {
rear == front
}
// 添加數據到隊列
def addQueue(n: Int): Unit = {
// 添加數據到隊列之前,先判斷隊列是否已滿
if (isFull()) {
println("隊列已滿,無法添加數據...")
return
}
rear += 1
arr(rear) = n
}
// 獲取對列數據
def getQueue(): Any = {
// 獲取隊列數據之前,先判斷隊列是否為空
if (isEmpty()) {
return new Exception("對列為空,無法獲取對列數據")
}
front += 1
return arr(front)
}
// 顯示隊列的所有數據
def showQueue(): Unit = {
// 顯示隊列數據之前,先判斷隊列是否為空
if (isEmpty()) {
println("隊列為空,沒有數據可顯示...")
return
}
// 遍歷隊列數據
for (i <- front + 1 to rear) {
printf("arr[%d]=%d\n", i, arr(i))
}
}
// 查看隊列的頭元素,但是不是改變隊列
def headQueue(): Any = {
if (isEmpty()) {
return new Exception("隊列為空,沒有頭元素可查看")
}
// 這里注意,不要去改變 fornt 值
return arr(front + 1)
}
}
對上面代碼的說明:雖然實現了隊列,但是數據空間不能復用,因此我們需要對其進行優化,使用取模的方式實現環形隊列。
19.4.4 數組模擬環形隊列
說明:
對前面的數組模擬隊列的優化,充分利用數組,因此將數組看做是一個環形的。(通過取模的方式來實現即可)
分析說明:
1、尾索引的下一個為頭索引時表示隊列滿,即將隊列容量空出一個作為約定,這個在做判斷隊列滿的時候需要注意 (rear + 1) % maxSize == front [表示隊列已滿]
2、rear == front [表示隊列為空]
代碼實現:
package com.atguigu.chapter19.queue
import scala.io.StdIn
/**
* 2、數組模擬環形隊列
*/
object ArrayQueueDemo02 {
def main(args: Array[String]): Unit = {
// 初始化一個隊列
val queue = new ArrayQueue(4)
var key = ""
while (true) {
println("show:表示顯示隊列")
println("exit:表示退出隊列")
println("add:表示添加數據到隊列")
println("get:表示取出隊列的數據")
println("head: 查看隊列頭的數據(不改變隊列)")
key = StdIn.readLine()
key match {
case "show" => queue.showQueue()
case "exit" => System.exit(0)
case "add" => {
println("請輸入一個數據(Int類型):")
val n = StdIn.readInt()
queue.addQueue(n)
}
case "get" => {
val res = queue.getQueue()
if (res.isInstanceOf[Exception]) {
println(res.asInstanceOf[Exception].getMessage)
} else {
println(s"取出對列的數據是 $res")
}
}
case "head" => {
val res = queue.headQueue()
if(res.isInstanceOf[Exception]) {
// 顯示錯誤信息
println(res.asInstanceOf[Exception].getMessage)
}else {
println("隊列頭元素的值為=" + res)
}
}
}
}
}
}
// 使用數組模擬環形隊列
class ArrayQueue(arrMaxSize: Int) {
// 該隊列的最大容量
val maxSize = arrMaxSize
// 該數組存放數據,模擬隊列
val arr = new Array[Int](maxSize)
// 記錄隊列前端
var front = 0 // front 是隊列最前元素的索引[含]
// 記錄隊列后端
var rear = 0 // rear 是隊列最后元素的索引[含]
// 判斷隊列是否已滿
def isFull(): Boolean = {
// 尾索引的下一個為頭索引時表示隊列滿,即將隊列容量空出一個作為約定,這個在做判斷隊列滿的時候需要注意
(rear + 1) % maxSize == front
}
// 判斷隊列是否為空
def isEmpty(): Boolean = {
rear == front
}
// 添加數據到隊列
def addQueue(n: Int): Unit = {
// 添加數據到隊列之前,先判斷隊列是否已滿
if (isFull()) {
println("隊列已滿,無法添加數據...")
return
}
arr(rear) = n
rear = (rear + 1) % maxSize // 將 rear 通過取模的方式后移,注意與 rear = rear + 1 的區別
}
// 獲取對列數據
def getQueue(): Any = {
// 獲取隊列數據之前,先判斷隊列是否為空
if (isEmpty()) {
return new Exception("對列為空,無法獲取對列數據")
}
val value = arr(front)
front = (front + 1) % maxSize // 將 front 通過取模的方式后移,注意與 front = front + 1 的區別
return value
}
// 顯示環形隊列的所有數據
def showQueue(): Unit = {
// 顯示隊列數據之前,先判斷隊列是否為空
if (isEmpty()) {
println("隊列為空,沒有數據可顯示...")
return
}
// 思路:從 front 取,取出幾個元素
for (i <- front until front + size()) {
printf("arr[%d]=%d\n", i % maxSize, arr(i % maxSize))
}
}
// 求出當前環形隊列有幾個元素
def size(): Int = {
// 算法
(rear + maxSize - front) % maxSize
}
// 查看隊列的頭元素,但是不是改變隊列
def headQueue(): Any = {
if (isEmpty()) {
return new Exception("隊列為空,沒有頭元素可查看")
}
// 這里注意,不要去改變 fornt 值
return arr(front)
}
}
19.5 鏈表 linked list
19.5.1 鏈表介紹
鏈表是有序的列表,但是它在內存中是存儲如下:
小結:
1、鏈表是一個有序列表。
2、鏈表的數據,在內存空間不一定是連續分布的。
19.5.2 單向鏈表的介紹
單向鏈表(帶頭結點) 邏輯結構示意圖如下:
所謂帶頭節點,就是鏈表的頭有一個 head 節點,該節點不存放具體的數據,只是為了操作方便而設計的這個節點。
19.5.3 單向鏈表的應用實例
使用帶 head 頭的單向鏈表實現:水滸英雄排行榜管理。完成對英雄人物的增刪改查操作。注:刪除、修改和查找可以考慮學員獨立完成。
第一種方式:在添加英雄時,直接添加到鏈表的尾部。
思路分析:
代碼實現:
示例代碼如下:
package com.atguigu.chapter19.linkedlist
import util.control.Breaks._
/**
* 1、無序插入單向鏈表節點,即 在添加英雄時,直接添加到鏈表的尾部。
*/
object SingleLinkedListDemo01 {
def main(args: Array[String]): Unit = {
// 測試單向鏈表的添加和遍歷
val heroNode1 = new HeroNode(1, "宋江", "及時雨")
val heroNode3 = new HeroNode(3, "吳用", "智多星")
val heroNode4 = new HeroNode(4, "公孫勝", "入雲龍")
val heroNode2 = new HeroNode(2, "盧俊義", "玉麒麟")
// 創建一個單向鏈表
val singleLinkedList = new SingleLinkedList
// 添加英雄
singleLinkedList.add(heroNode1)
singleLinkedList.add(heroNode3)
singleLinkedList.add(heroNode4)
singleLinkedList.add(heroNode2)
// 遍歷英雄
singleLinkedList.list()
}
}
// 定義單向鏈表,用來管理 Hero
class SingleLinkedList {
// 先初始化一個頭結點,頭結點一般不用
val head = new HeroNode(0, "", "")
// 編寫添加英雄的方法
// 第一種方式:在添加英雄時,直接添加到鏈表的尾部。
def add(heroNode: HeroNode): Unit = {
// 因為頭結點不能動,因此我們需要有一個臨時節點作為輔助,即使用 temp 也指向 new HeroNode(0, "", "") 的地址
var temp = head
// 先找到該鏈表的最后
breakable {
while (true) {
if (temp.next == null) {
break()
}
// 如果沒有到鏈表最后,接着找
temp = temp.next
}
}
// 當退出 while 循環后,temp 指向的就是鏈表的最后
temp.next = heroNode // 在鏈表的最后將 英雄對象的地址 賦值給 temp
}
// 遍歷單向鏈表
def list(): Unit = {
// 先判斷當前列表是否為空
if (head.next == null) {
println("鏈表為空!")
return
}
// 因為頭結點不能動,因此我們需要有一個臨時節點作為輔助
// 又因為 head 節點的數據我們不關心,因此這里使得 temp 指向 head 的下一個地址(我們真正的數據)
var temp = head.next
breakable {
while (true) {
if (temp == null) {
break()
}
printf("節點信息:no=%d name=%s nickname=%s\n", temp.no, temp.name, temp.nickname)
temp = temp.next
}
}
}
}
// 先創建 HeroNode
class HeroNode(hNo: Int, hName: String, hNickname: String) {
var no: Int = hNo
var name: String = hName
var nickname: String = hNickname
var next: HeroNode = null // next 默認為 null
}
輸出結果如下:
節點信息:no=1 name=宋江 nickname=及時雨
節點信息:no=3 name=吳用 nickname=智多星
節點信息:no=4 name=公孫勝 nickname=入雲龍
節點信息:no=2 name=盧俊義 nickname=玉麒麟
第二種方式:在添加英雄時,根據排名將英雄插入到指定位置。
示例代碼如下:
package com.atguigu.chapter19.linkedlist
import util.control.Breaks._
/**
* 2、有序插入單向鏈表節點,即 在添加英雄時,根據排名將英雄插入到指定位置。(如果有這個排名,則添加失敗,並給出提示)
*/
object SingleLinkedListDemo02 {
def main(args: Array[String]): Unit = {
// 測試單向鏈表的添加和遍歷
val heroNode1 = new HeroNode2(1, "宋江", "及時雨")
val heroNode3 = new HeroNode2(3, "吳用", "智多星")
val heroNode4 = new HeroNode2(4, "公孫勝", "入雲龍")
val heroNode2 = new HeroNode2(2, "盧俊義", "玉麒麟")
// 創建一個單向鏈表
val singleLinkedList2 = new SingleLinkedList2
// 添加英雄
singleLinkedList2.add(heroNode1)
singleLinkedList2.add(heroNode3)
singleLinkedList2.add(heroNode4)
singleLinkedList2.add(heroNode2)
// 遍歷英雄
singleLinkedList2.list()
}
}
// 定義單向鏈表,用來管理 Hero
class SingleLinkedList2 {
// 先初始化一個頭結點,頭結點一般不用
val head = new HeroNode2(0, "", "")
// 編寫添加英雄的方法
// 第二種方式:在添加英雄時,根據排名將英雄插入到指定位置。(如果有這個排名,則添加失敗,並給出提示)
def add(heroNode: HeroNode2): Unit = {
// 因為頭結點不能動,因此我們需要有一個臨時節點作為輔助,即使用 temp 也指向 new HeroNode2(0, "", "") 的地址
// 注意:我們在找這個添加位置時,是將這個節點加入到節點 temp 的后面
// 因此,在比較時,是將當前的節點 heroNode 和節點 temp.next 比較
var temp = head
var flag = false // flag 用於判斷該英雄的編號是否已存在
breakable {
while (true) {
if (temp.next == null) { // 說明節點 temp 已經是鏈表的最后
break()
}
if (heroNode.no < temp.next.no) { // 說明位置找到,當前這個節點 heroNode 應加入到 節點 temp 的后面,節點 temp.next 的前面
break()
} else if (heroNode.no == temp.next.no) { // 說明已經有該節點
flag = true
break()
}
temp = temp.next
}
}
if (flag) { //
printf("待添加的英雄編號 %d 已經存在,不能加入\n", heroNode.no)
} else {
// 加入英雄,注意:添加順序
heroNode.next = temp.next
temp.next = heroNode
}
}
// 遍歷單向鏈表
def list(): Unit = {
// 先判斷當前列表是否為空
if (head.next == null) {
println("鏈表為空!")
return
}
// 因為頭結點不能動,因此我們需要有一個臨時節點作為輔助
// 又因為 head 節點的數據我們不關心,因此這里使得 temp 指向 head 的下一個地址(我們真正的數據)
var temp = head.next
breakable {
while (true) {
if (temp == null) {
break()
}
printf("節點信息:no=%d name=%s nickname=%s\n", temp.no, temp.name, temp.nickname)
temp = temp.next
}
}
}
}
// 先創建 HeroNode2
class HeroNode2(hNo: Int, hName: String, hNickname: String) {
var no: Int = hNo
var name: String = hName
var nickname: String = hNickname
var next: HeroNode2 = null // next 默認為 null
}
輸出結果如下:
節點信息:no=1 name=宋江 nickname=及時雨
節點信息:no=2 name=盧俊義 nickname=玉麒麟
節點信息:no=3 name=吳用 nickname=智多星
節點信息:no=4 name=公孫勝 nickname=入雲龍
練習:
1、修改節點的值,根據編號的值進行修改(即編號不能變)
2、將整個節點替換(即重新指向)
3、刪除節點(根據編號刪除)
示例代碼如下:
package com.atguigu.chapter19.linkedlist
import util.control.Breaks._
/**
* 2、有序插入單向鏈表節點,即 在添加英雄時,根據排名將英雄插入到指定位置。(如果有這個排名,則添加失敗,並給出提示)
*/
object SingleLinkedListDemo02 {
def main(args: Array[String]): Unit = {
// 測試單向鏈表的添加和遍歷
val heroNode1 = new HeroNode2(1, "宋江", "及時雨")
val heroNode3 = new HeroNode2(3, "吳用", "智多星")
val heroNode4 = new HeroNode2(4, "公孫勝", "入雲龍")
val heroNode2 = new HeroNode2(2, "盧俊義", "玉麒麟")
// 創建一個單向鏈表
val singleLinkedList2 = new SingleLinkedList2
println("----------添加節點(有序添加)----------------")
// 添加英雄
singleLinkedList2.add(heroNode1)
singleLinkedList2.add(heroNode3)
singleLinkedList2.add(heroNode4)
singleLinkedList2.add(heroNode2)
// 遍歷英雄
singleLinkedList2.list()
println("----------修改節點的值-----------------------")
// 修改節點的值
val heroNode5 = new HeroNode2(1, "宋公明", "山東及時雨")
singleLinkedList2.update(heroNode5)
// 遍歷英雄
singleLinkedList2.list()
println("----------修改節點的值(全部替換)------------")
// 修改節點的值(全部替換)
val heroNode6 = new HeroNode2(2, "盧員外", "河北玉麒麟")
singleLinkedList2.update2(heroNode6)
// 遍歷英雄
singleLinkedList2.list()
println("----------刪除節點--------------------------")
// 刪除節點
singleLinkedList2.del(4)
// 遍歷英雄
singleLinkedList2.list()
}
}
// 定義單向鏈表,用來管理 Hero
class SingleLinkedList2 {
// 先初始化一個頭結點,頭結點一般不用
val head = new HeroNode2(0, "", "")
// 刪除節點(根據編號刪除)
def del(no: Int): Unit = {
var temp = head
var flag = false
breakable {
while (true) {
if (temp.next == null) {
break()
}
if (temp.next.no == no) { // 找到節點
flag = true
break()
}
temp = temp.next
}
}
if (flag) {
// 刪除節點
temp.next = temp.next.next
} else {
printf("要刪除的 no=%d 節點不存在\n", no)
}
}
// 將整個節點替換(即重新指向)
def update2(heroNode: HeroNode2): Unit = {
// 先判斷當前列表是否為空
if (head.next == null) {
println("鏈表為空,不能修改!")
return
}
// 先找到節點
var temp = head.next
var flag = false
breakable {
while (true) {
if (temp == null) { // 沒有找到節點
break()
}
if (temp.no == heroNode.no) { // 找到節點
flag = true
break()
}
temp = temp.next
}
}
if (flag) {
del(temp.no)
add(heroNode)
} else {
printf("沒有找到編號為 %d 的節點,不能修改!\n", heroNode.no)
}
}
// 修改節點的值,根據編號的值進行修改(即編號不能變)
def update(heroNode: HeroNode2): Unit = {
// 先判斷當前列表是否為空
if (head.next == null) {
println("鏈表為空,不能修改!")
return
}
// 先找到節點
var temp = head.next
var flag = false
breakable {
while (true) {
if (temp == null) { // 沒有找到節點
break()
}
if (temp.no == heroNode.no) { // 找到節點
flag = true
break()
}
temp = temp.next
}
}
if (flag) {
temp.name = heroNode.name
temp.nickname = heroNode.nickname
} else {
printf("沒有找到編號為 %d 的節點,不能修改!\n", heroNode.no)
}
}
// 編寫添加英雄的方法
// 第二種方式:在添加英雄時,根據排名將英雄插入到指定位置。(如果有這個排名,則添加失敗,並給出提示)
def add(heroNode: HeroNode2): Unit = {
// 因為頭結點不能動,因此我們需要有一個臨時節點作為輔助,即使用 temp 也指向 new HeroNode2(0, "", "") 的地址
// 注意:我們在找這個添加位置時,是將這個節點加入到節點 temp 的后面
// 因此,在比較時,是將當前的節點 heroNode 和節點 temp.next 比較
var temp = head
var flag = false // flag 用於判斷該英雄的編號是否已存在
breakable {
while (true) {
if (temp.next == null) { // 說明節點 temp 已經是鏈表的最后
break()
}
if (heroNode.no < temp.next.no) { // 說明位置找到,當前這個節點 heroNode 應加入到 節點 temp 的后面,節點 temp.next 的前面
break()
} else if (heroNode.no == temp.next.no) { // 說明已經有該節點
flag = true
break()
}
temp = temp.next
}
}
if (flag) { //
printf("待添加的英雄編號 %d 已經存在,不能加入\n", heroNode.no)
} else {
// 加入英雄,注意:添加順序
heroNode.next = temp.next
temp.next = heroNode
}
}
// 遍歷單向鏈表
def list(): Unit = {
// 先判斷當前列表是否為空
if (head.next == null) {
println("鏈表為空!")
return
}
// 因為頭結點不能動,因此我們需要有一個臨時節點作為輔助
// 又因為 head 節點的數據我們不關心,因此這里使得 temp 指向 head 的下一個地址(我們真正的數據)
var temp = head.next
breakable {
while (true) {
if (temp == null) {
break()
}
printf("節點信息:no=%d name=%s nickname=%s\n", temp.no, temp.name, temp.nickname)
temp = temp.next
}
}
}
}
// 先創建 HeroNode2
class HeroNode2(hNo: Int, hName: String, hNickname: String) {
var no: Int = hNo
var name: String = hName
var nickname: String = hNickname
var next: HeroNode2 = null // next 默認為 null
}
輸出結果如下:
----------添加節點(有序添加)----------------
節點信息:no=1 name=宋江 nickname=及時雨
節點信息:no=2 name=盧俊義 nickname=玉麒麟
節點信息:no=3 name=吳用 nickname=智多星
節點信息:no=4 name=公孫勝 nickname=入雲龍
----------修改節點的值-----------------------
節點信息:no=1 name=宋公明 nickname=山東及時雨
節點信息:no=2 name=盧俊義 nickname=玉麒麟
節點信息:no=3 name=吳用 nickname=智多星
節點信息:no=4 name=公孫勝 nickname=入雲龍
----------修改節點的值(全部替換)------------
節點信息:no=1 name=宋公明 nickname=山東及時雨
節點信息:no=2 name=盧員外 nickname=河北玉麒麟
節點信息:no=3 name=吳用 nickname=智多星
節點信息:no=4 name=公孫勝 nickname=入雲龍
----------刪除節點--------------------------
節點信息:no=1 name=宋公明 nickname=山東及時雨
節點信息:no=2 name=盧員外 nickname=河北玉麒麟
節點信息:no=3 name=吳用 nickname=智多星
19.5.4 雙向鏈表的應用實例
使用帶 head 頭的雙向鏈表實現:水滸英雄排行榜管理。
單向鏈表的缺點分析:
1、單向鏈表,查找的方向只能是一個方向,而雙向鏈表可以向前或者向后查找。
2、單向鏈表不能自我刪除,需要靠輔助節點,而雙向鏈表,則可以自我刪除,所以前面我們單鏈表刪除時節點,總是找到 temp 的下一個節點來刪除的(認真體會)。
3、示意圖幫助理解刪除。
將前面的單向鏈表改成雙向鏈表
雙向鏈表刪除圖解
示例代碼如下:
package com.atguigu.chapter19.linkedlist
import util.control.Breaks.{break, breakable}
/**
* 雙向鏈表
*/
object DoubleLinkedListDemo01 {
def main(args: Array[String]): Unit = {
// 測試雙向鏈表的添加和遍歷
println("----------添加節點(無序添加)----------------")
val heroNode1 = new HeroNode3(1, "宋江", "及時雨")
val heroNode3 = new HeroNode3(3, "吳用", "智多星")
val heroNode4 = new HeroNode3(4, "公孫勝", "入雲龍")
val heroNode2 = new HeroNode3(2, "盧俊義", "玉麒麟")
// 創建一個雙向鏈表
val doubleLinkedList = new DoubleLinkedList
// 添加英雄
doubleLinkedList.add(heroNode1)
doubleLinkedList.add(heroNode3)
doubleLinkedList.add(heroNode4)
doubleLinkedList.add(heroNode2)
// 遍歷英雄
doubleLinkedList.list()
println("----------修改節點的值-----------------------")
// 修改節點的值
val heroNode5 = new HeroNode3(1, "宋公明", "山東及時雨")
doubleLinkedList.update(heroNode5)
// 遍歷英雄
doubleLinkedList.list()
println("----------刪除節點--------------------------")
// 刪除節點
doubleLinkedList.del(2)
doubleLinkedList.del(3)
doubleLinkedList.del(4)
// 遍歷英雄
doubleLinkedList.list()
println("----------再次添加節點----------------------")
doubleLinkedList.add(heroNode3)
// 遍歷英雄
doubleLinkedList.list()
}
}
// 定義雙向鏈表,用來管理 Hero
class DoubleLinkedList {
// 先初始化一個頭結點,頭結點一般不用(不會動)
val head = new HeroNode3(0, "", "")
// 添加-遍歷-修改-刪除
// 編寫添加英雄的方法
// 第一種方式:在添加英雄時,直接添加到鏈表的尾部。
def add(heroNode: HeroNode3): Unit = {
// 因為頭結點不能動,因此我們需要有一個臨時節點作為輔助,即使用 temp 也指向 new HeroNode3(0, "", "") 的地址
var temp = head
// 先找到該鏈表的最后
breakable {
while (true) {
if (temp.next == null) {
break()
}
// 如果沒有到鏈表最后,接着找
temp = temp.next
}
}
// 當退出 while 循環后,temp 指向的就是鏈表的最后
temp.next = heroNode // 在鏈表的最后將 英雄對象的地址 賦值給 temp
heroNode.pre = temp
}
// 遍歷雙向鏈表
def list(): Unit = {
// 先判斷當前列表是否為空
if (head.next == null) {
println("鏈表為空!")
return
}
// 因為頭結點不能動,因此我們需要有一個臨時節點作為輔助
// 又因為 head 節點的數據我們不關心,因此這里使得 temp 指向 head 的下一個地址(我們真正的數據)
var temp = head.next
breakable {
while (true) {
// 判斷是否到最后
if (temp == null) {
break()
}
printf("節點信息:no=%d name=%s nickname=%s\n", temp.no, temp.name, temp.nickname)
temp = temp.next
}
}
}
// 修改節點的值,根據編號的值進行修改(即編號不能變)
def update(heroNode: HeroNode3): Unit = {
// 先判斷當前列表是否為空
if (head.next == null) {
println("鏈表為空,不能修改!")
return
}
// 先找到節點
var temp = head.next
var flag = false
breakable {
while (true) {
if (temp == null) { // 沒有找到節點
break()
}
if (temp.no == heroNode.no) { // 找到節點
flag = true
break()
}
temp = temp.next
}
}
if (flag) {
temp.name = heroNode.name
temp.nickname = heroNode.nickname
} else {
printf("沒有找到編號為 %d 的節點,不能修改!\n", heroNode.no)
}
}
// 刪除節點(根據編號刪除)
// 利用雙向鏈表可以實現自我刪除的特點
def del(no: Int): Unit = {
// 先判斷當前列表是否為空
if (head.next == null) {
println("鏈表為空,不能刪除!")
return
}
// 輔助節點
var temp = head.next
var flag = false
breakable {
while (true) {
if (temp == null) {
break()
}
if (temp.no == no) { // 找到節點
flag = true
break()
}
temp = temp.next
}
}
if (flag) {
// 刪除節點
temp.pre.next = temp.next
if (temp.next != null) {
temp.next.pre = temp.pre
temp.pre = null
temp.next = null
} else {
temp.pre = null
}
} else {
printf("要刪除的 no=%d 節點不存在\n", no)
}
}
}
// 先創建 HeroNode3
class HeroNode3(hNo: Int, hName: String, hNickname: String) {
var no: Int = hNo
var name: String = hName
var nickname: String = hNickname
var pre: HeroNode3 = null // pre 默認為 null
var next: HeroNode3 = null // next 默認為 null
}
輸出結果如下:
----------添加節點(無序添加)----------------
節點信息:no=1 name=宋江 nickname=及時雨
節點信息:no=3 name=吳用 nickname=智多星
節點信息:no=4 name=公孫勝 nickname=入雲龍
節點信息:no=2 name=盧俊義 nickname=玉麒麟
----------修改節點的值-----------------------
節點信息:no=1 name=宋公明 nickname=山東及時雨
節點信息:no=3 name=吳用 nickname=智多星
節點信息:no=4 name=公孫勝 nickname=入雲龍
節點信息:no=2 name=盧俊義 nickname=玉麒麟
----------刪除節點--------------------------
節點信息:no=1 name=宋公明 nickname=山東及時雨
----------再次添加節點----------------------
節點信息:no=1 name=宋公明 nickname=山東及時雨
節點信息:no=3 name=吳用 nickname=智多星
19.5.5 單向環形鏈表的應用場景
Josephu 問題(丟手帕問題):設編號為1,2,…,n 的 n 個人圍坐一圈,約定編號為 k(1<=k<=n)的人從 1 開始報數,數到 m 的那個人出列,它的下一位又從 1 開始報數,數到 m 的那個人又出列,依次類推,直到所有人出列為止,由此產生一個出隊編號的序列。
提示:用一個不帶頭結點的循環鏈表來處理 Josephu 問題。
問題:先構成一個有 n 個結點的單循環鏈表,然后由 k 結點起從 1 開始計數,計到 m 時,對應結點從鏈表中刪除,然后再從被刪除結點的下一個結點又從 1 開始計數,直到最后一個結點從鏈表中刪除算法結束。
示意圖說明:
思路分析:
示例代碼如下:
package com.atguigu.chapter19.linkedlist
import util.control.Breaks._
object JosephuDemo {
def main(args: Array[String]): Unit = {
// 創建一個雙向鏈表 BoyGame
val boyGame = new BoyGame
boyGame.addBoy(7)
boyGame.showBoy()
println("--------------------")
boyGame.countBoy(4, 3, 7)
}
}
// 定義單向鏈表,用來管理 Boy
class BoyGame {
// 先初始化一個頭結點,頭結點一般不用(不會動)
var first: Boy = null
// 添加 Boy,形成一個單向環形鏈表
// nums 表示共有幾個小孩
def addBoy(nums: Int): Unit = {
if (nums < 1) {
println("Boy的個數不正確")
return
}
// 因為頭結點不能動,因此我們需要有一個臨時節點作為輔助,只是該輔助節點的指向是null,即是一個沒有指向任何地址的指針
var temp: Boy = null
for (no <- 1 to nums) {
// 根據編號創建 Boy 對象
val boy = new Boy(no)
// 如果是第一個 Boy,則自己指向自己,並將 temp 也指向 第一個 Boy
if (no == 1) {
first = boy
boy.next = first
temp = first // 輔助指針指向到 第一個 Boy,即 first
} else {
temp.next = boy // 輔助指針指向 當前的 Boy
boy.next = first // 當前的 Boy 指向 第一個 Boy
temp = boy // 輔助指針指向下一個 Boy
}
}
}
// 遍歷單向環形鏈表
def showBoy(): Unit = {
if (first.next == null) {
println("沒有Boy")
return
}
// 因為頭結點不能動,因此我們需要有一個臨時節點作為輔助
// 又因為 first 節點的數據跟我們有關,因此這里使得 temp 指向 first 的地址
var temp = first
breakable {
while (true) {
printf("Boy 的編號是 %d\n", temp.no)
if (temp.next == first) {
break()
}
temp = temp.next // 移動指針到下一個 Boy
}
}
}
// 編寫 countBoy
// startNo 從第幾個人開始數
// countNum 數幾下
// nums 總人數
def countBoy(startNo: Int, countNum: Int, nums: Int): Unit = {
// 對參數進行判斷
if (first.next == null || startNo < 1 || startNo > nums) {
println("參數有誤,請重新輸入!")
return
}
// 思路:
// 1、在 first 前面設計一個輔助指針 temp,即將 temp 指針定位到 first 前面
var temp = first // 輔助指針
breakable {
while (true) { // 遍歷一圈單向環形鏈表后,找到指針 first 的前一個位置,此時是 新temp
if (temp.next == first) {
break()
}
temp = temp.next // 移動指針
}
}
// 2、將 first 指針移動到 startNo 位置,將 temp 指針移動到 startNo - 1 位置
for (i <- 1 until startNo) {
first = first.next
temp = temp.next
}
breakable {
while (true) {
if (temp == first) {
break()
}
// 3、開始數 countNum 個位置, first 和 temp 指針對應移動
for (i <- 1 until countNum) {
first = first.next
temp = temp.next
}
printf("Boy %d 號出圈\n", first.no)
// 4、刪除 first 指向的節點,並移動 first 指針到下一節點,temp 指針對應移動
// first = first.next
temp.next = first.next
first = first.next
}
}
// while 循環結束后,只有一個人了
printf("最后一個人是Boy %d 號", first.no)
}
}
// 定義 Boy 類
class Boy(bNo: Int) {
var no: Int = bNo
var next: Boy = null
}
輸出結果如下:
Boy 的編號是 1
Boy 的編號是 2
Boy 的編號是 3
Boy 的編號是 4
Boy 的編號是 5
Boy 的編號是 6
Boy 的編號是 7
--------------------
Boy 6 號出圈
Boy 2 號出圈
Boy 5 號出圈
Boy 3 號出圈
Boy 1 號出圈
Boy 4 號出圈
最后一個人是Boy 7 號
19.6 棧 stack
19.6.1 看一個實際需求
請輸入一個表達式
計算式:[722-5+1-5+3-3] 點擊計算,[如下圖]
請問:計算機底層是如何運算得到結果的?
注意:不是簡單的把算式列出運算,因為我們看這個算式 7 * 2 * 2 - 5, 但是計算機怎么理解這個算式的(對計算機而言,它接收到的就是一個字符串),我們討論的是這個問題。
19.6.2 棧的介紹
1、棧的英文為(stack)。
2、棧是一個先入后出(FILO:First In Last Out)的有序列表。
3、棧(stack)是限制線性表中元素的插入和刪除只能在線性表的同一端進行的一種特殊線性表。允許插入和刪除的一端,為變化的一端,稱為棧頂(Top),另一端為固定的一端,稱為棧底(Bottom)。
4、根據堆棧的定義可知,最先放入棧中元素在棧底,最后放入的元素在棧頂,而刪除元素剛好相反,最后放入的元素最先刪除,最先放入的元素最后刪除。
出棧和入棧的概念(如圖所示)
入棧
出棧
19.6.3 棧的幾個經典的應用場景
1、子程序的調用:在跳往子程序前,會先將下個指令的地址存到堆棧中,直到子程序執行完后再將地址取出,以回到原來的程序中。
2、處理遞歸調用:和子程序的調用類似,只是除了儲存下一個指令的地址外,也將參數、區域變量等數據存入堆棧中。
3、表達式的轉換與求值(實際解決)。
4、二叉樹的遍歷。
5、圖形的深度優先(depth-first)搜索法。
19.6.4 棧的快速入門
用數組模擬棧的使用
由於棧是一種有序列表,當然可以使用數組的結構來儲存棧的數據內容,下面我們就用數組模擬棧的出棧、入棧等操作。實現思路分析,並畫出示意圖,如下:
示例代碼如下:
package com.atguigu.chapter19.stack
import scala.io.StdIn
/**
* 1、用數組模擬棧的使用
*/
object ArrayStackDemo01 {
def main(args: Array[String]): Unit = {
// 測試棧的基本使用
val arrayStack = new ArrayStack(3)
var key = ""
while (true) {
println("list:表示顯示棧的數據")
println("exit:表示退出程序")
println("push:表示將數據壓棧")
println("pop:表示將數據彈棧")
key = StdIn.readLine()
key match {
case "list" => arrayStack.list()
case "exit" => System.exit(0)
case "push" => {
print("請輸入一個數據(Int類型):")
val n = StdIn.readInt()
arrayStack.push(n)
}
case "pop" => arrayStack.pop()
}
}
}
}
// 用數組模擬棧的使用
class ArrayStack(size: Int) {
// 棧的大小
val maxSize = size
var stack = new Array[Int](maxSize)
// 棧頂,初始化為 -1
var top = -1
// 判斷是否棧滿
def isFull(): Boolean = {
top == maxSize - 1
}
// 判斷是否棧空
def isEmpty(): Boolean = {
top == -1
}
// 將數據壓入棧的方法
def push(value: Int): Unit = {
if (isFull()) {
println("棧滿,不能再存放數據")
return
}
top += 1
stack(top) = value
}
// 將數據彈出棧的方法
def pop(): Any = {
if (isEmpty()) {
println("棧空,不能再取出數據")
return
}
val value = stack(top)
top -= 1
return value
}
// 遍歷棧(從棧頂往下取出)
def list(): Unit = {
if (isEmpty()) {
println("棧空,沒有數據可顯示")
return
}
for (i <- 0 to top reverse) {
printf("stack[%d]=%d\n", i, stack(i))
}
}
}
使用鏈表來模擬棧的使用
有空做做
19.6.5 棧實現綜合計算器
代碼實現的思路分析
示例代碼如下:
package com.atguigu.chapter19.stack
import util.control.Breaks._
/**
* 2、完成多位數表達式的計算,例如:30+2*6-2 7*2*2-5+1-5+3-4
*/
object CalculatorDemo02 {
def main(args: Array[String]): Unit = {
// 數值棧
val numStack = new ArrayStack3(20)
// 符號棧
val operStack = new ArrayStack3(20)
/*
expression = "3+2*6-2"
思路:
1、設計兩個棧:數值棧,符號棧
2、對 expresson 進行掃描,一個一個的取出
3、當取出的字符是數值時,就直接入數值棧
4、當取出的字符是符號時:
4.1 如果當前符號棧中沒有數據,就直接入棧
4.2 如果當前符號的優先級小於等於符號棧的棧頂的符號的優先級,
則pop出該符號,並從數值棧中一次彈出兩個數據,進行運算,
將結果重新push到數值棧,再將當前符號push到符號棧
4.3 反之直接入符號棧
5、當整個表達式掃描完畢后,依次從數值棧和符號棧中取出數據,
進行運算,最后在數值棧中的數據就是結果
*/
// val expression = "30+2*6-2"
val expression = "7*2*2-5+1-5+3-4"
var index = 0
var num1 = 0
var num2 = 0
var oper = 0
var res = 0
var char = ' '
var keepNum = "" // 在進行掃描時,保存上次的數字char,並進行拼接
// 循環的取出 expression 的字符
breakable {
while (true) {
// 1、設計兩個棧:數值棧,符號棧
// 2、對 expresson 進行掃描,一個一個的取出
char = (expression.substring(index, index + 1)) (0)
if (operStack.isOper(char)) { // 如果當前符號是一個操作符
if (!operStack.isEmpty()) { // 如果當前符號棧中有數據
// 當前符號的優先級小於等於符號棧的棧頂的符號的優先級
if (operStack.priority(char) <= operStack.priority(operStack.stack(operStack.top))) {
// 開始計算
num1 = numStack.pop().toString.toInt
num2 = numStack.pop().toString.toInt
oper = operStack.pop().toString.toInt
res = numStack.cal(num1, num2, oper)
// 將計算的結果入數值棧
numStack.push(res)
// 將操作符壓入符號棧
operStack.push(char)
} else {
// 反之直接入符號棧
operStack.push(char)
}
} else {
operStack.push(char)
}
} else { // 是一個數
// 處理多位數的邏輯
keepNum += char
// 如果 char 已經是 expression 的最后一個字符,則該數直接入棧
if (index == expression.length - 1) {
numStack.push(keepNum.toInt)
} else {
// 判斷 char 的下一個字符是不是數字,如果是數字,則進行下一次掃描,如果是操作符,就該數直接入棧
if (operStack.isOper(expression.substring(index + 1, index + 2)(0))) { // 是操作符,就該數直接入棧
numStack.push(keepNum.toInt)
keepNum = "" // 清空
}
}
// numStack.push(char - 48) // '1' => 49
// numStack.push((char + "").toInt)
}
// index 后移
index += 1
if (index >= expression.length) {
break()
}
}
}
// 5、當整個表達式掃描完畢后,依次從數值棧和符號棧中取出數據,進行運算,最后在數值棧中的數據就是結果
breakable {
while (true) {
if (operStack.isEmpty()) {
break()
}
// 開始計算
num1 = numStack.pop().toString.toInt
num2 = numStack.pop().toString.toInt
oper = operStack.pop().toString.toInt
res = numStack.cal(num1, num2, oper)
// 將計算的結果入數值棧
numStack.push(res)
}
}
printf("表達式: %s = %d", expression, numStack.pop().toString.toInt)
}
}
// 用數組模擬棧的使用,該棧已經測試過了,可以使用
class ArrayStack3(size: Int) {
// 棧的大小
val maxSize = size
var stack = new Array[Int](maxSize)
// 棧頂,初始化為 -1
var top = -1
// 判斷是否棧滿
def isFull(): Boolean = {
top == maxSize - 1
}
// 判斷是否棧空
def isEmpty(): Boolean = {
top == -1
}
// 將數據壓入棧的方法
def push(value: Int): Unit = {
if (isFull()) {
println("棧滿,不能再存放數據")
return
}
top += 1
stack(top) = value
}
// 將數據彈出棧的方法
def pop(): Any = {
if (isEmpty()) {
println("棧空,不能再取出數據")
return
}
val value = stack(top)
top -= 1
return value
}
// 遍歷棧(從棧頂往下取出)
def list(): Unit = {
if (isEmpty()) {
println("棧空,沒有數據可顯示")
return
}
for (i <- 0 to top reverse) {
printf("stack[%d]=%d\n", i, stack(i))
}
}
// 自定義運算符的優先級,這里我們先簡單定義下符號的優先級
// +- => 0 * / => 1 數字越大優先級越高
def priority(oper: Int): Int = {
if (oper == '*' || oper == '/') {
return 1
} else if (oper == '+' || oper == '-') {
return 0
} else {
return -1 // 運算符不正確
}
}
// 判斷是否是一個操作符(即符號)
def isOper(value: Int): Boolean = {
value == '+' || value == '-' || value == '*' || value == '/'
}
// 計算的方法,這里我們僅考慮整數的計算
def cal(num1: Int, num2: Int, oper: Int): Int = {
var res = 0
oper match {
case '+' => res = num2 + num1
case '-' => res = num2 - num1
case '*' => res = num2 * num1
case '/' => res = num2 / num1
}
res
}
}
輸出結果如下:
表達式: 7*2*2-5+1-5+3-4 = 18
19.7 遞歸 recursive
19.7.1 看個實際應用場景
迷宮問題(回溯)
19.7.2 遞歸的概念
簡單的說:遞歸就是函數/方法自己調用自己,每次調用時傳入不同的變量,遞歸有助於編程者解決復雜的問題,同時可以讓代碼變得簡潔。
19.7.3 遞歸快速入門
我列舉兩個小案例,來幫助大家理解遞歸,遞歸在講函數時已經講過(當時講的相對比較簡單),這里在給大家回顧一下遞歸調用機制
1、打印問題
2、階乘問題
思路分析
if (5 > 2) {
if (4 > 2) {
if (3 > 2) {
if (2 > 2) {
}
println("n=" + 2)
}
println("n=" + 3)
}
println("n=" + 4)
}
println("n=" + 5)
打印代碼如下:
package com.atguigu.chapter19.recursive
object Demo01 {
def main(args: Array[String]): Unit = {
test1(5)
println("----------")
test2(5)
println("----------")
test3(5)
}
def test1(n: Int): Unit = {
if (n > 2) {
test1(n - 1)
}
println("n=" + n)
}
def test2(n: Int): Unit = {
println("n=" + n)
if (n > 2) {
test2(n - 1)
}
}
def test3(n: Int): Unit = {
if (n > 2) {
test3(n - 1)
println("n=" + n)
}
}
}
輸出結果如下:
n=2
n=3
n=4
n=5
----------
n=5
n=4
n=3
n=2
----------
n=3
n=4
n=5
求階乘代碼如下:
package com.atguigu.chapter19.recursive
object Demo02 {
def main(args: Array[String]): Unit = {
println(factorial(3))
}
// 階乘
def factorial(n: Int): Int = {
if (n == 1) {
1
} else {
factorial(n - 1) * n
}
}
}
輸出結果如下:
6
19.7.4 遞歸用於解決什么樣的問題
1、各種數學問題如: 8 皇后問題、漢諾塔、階乘問題、迷宮問題、球和籃子的問題(google 編程大賽,有空看看)
2、將用棧解決的問題 -> 遞歸代碼比較簡潔
19.7.5 遞歸需要遵守的重要原則
1、執行一個函數時,就創建一個新的受保護的獨立空間(新函數棧)。
2、函數的局部變量是獨立的,不會相互影響。
3、遞歸必須向退出遞歸的條件逼近,否則就是無限遞歸,死龜了:)。
4、當一個函數執行完畢,或者遇到 return,就會返回,遵守誰調用,就將結果返回給誰,同時當函數執行完畢或者返回時,該函數本身也會被系統銷毀。
19.7.6 舉一個比較綜合的案例-迷宮問題
解釋說明
1、小球得到的路徑,和程序員設置的找路策略有關,即:找路的上下左右的順序相關。
2、再得到小球路徑時,可以先使用(下右上左),再改成(上右下左),看看路徑是不是有變化。
3、測試回溯現象。
4、思考: 如何求出最短路徑?
思路分析
代碼實現
package com.atguigu.chapter19.recursive
/**
* 迷宮問題思路分析:
* 1、創建一個二維數組(表示地圖)
* 2、約定元素的值:0表示可以走還沒有走,1表示牆,2表示可以走,3表示已經走過,但是是死路。
* 3、確定一個策略:下->右->上->左
* 4、代碼
*/
object MiGongDemo01 {
def main(args: Array[String]): Unit = {
// 創建地圖
val map = Array.ofDim[Int](8, 7)
// 上下全部置1,1表示牆
for (i <- 0 until 7) {
map(0)(i) = 1
map(7)(i) = 1
}
// 左右全部置1,1表示牆
for (i <- 0 until 8) {
map(i)(0) = 1
map(i)(6) = 1
}
map(3)(1) = 1
map(3)(2) = 1
// 打印地圖
for (i <- 0 until 8) {
for (j <- 0 until 7) {
print(map(i)(j) + " ")
}
println()
}
/* println("----------策略為:下->右->上->左----------")
// 測試遞歸回溯方法
findWay1(map, 1, 1)
// 打印地圖
for (i <- 0 until 8) {
for (j <- 0 until 7) {
print(map(i)(j) + " ")
}
println()
}*/
println("----------策略為:上->右->下->左----------")
// 測試遞歸回溯方法
findWay2(map, 1, 1)
// 打印地圖
for (i <- 0 until 8) {
for (j <- 0 until 7) {
print(map(i)(j) + " ")
}
println()
}
// 使用遞歸回溯來找路,確定一個策略為:下->右->上->左
// map 表示地圖,i j 是指定從地圖的哪個點開始出發,測試的時候我們指定為 (1, 1)
def findWay1(map: Array[Array[Int]], i: Int, j: Int): Boolean = {
if (map(6)(5) == 2) { // 表示路已經通了
return true
} else {
if (map(i)(j) == 0) { // 0表示可以走還沒有走
// 開始遞歸回溯(試探性判斷)
map(i)(j) = 2 // 先假定該點(1, 1)可以走通
if (findWay1(map, i + 1, j)) { // 先向下找,可以走通
return true
} else if (findWay1(map, i, j + 1)) { // 先向下找,不可以走通,則向右找,可以走通
return true
} else if (findWay1(map, i - 1, j)) { // 先向下找,不可以走通,則向右找,不可以走通,則向上找,可以走通
return true
} else if (findWay1(map, i, j - 1)) { // 向 下 右 上 不可以走通,則向左走,可以走通
return true
} else { // 都不可以走通
map(i)(j) = 3
return false
}
} else { // map(i)(j) == 1或者2或者3
return false
}
}
}
// 使用遞歸回溯來找路,確定一個新的策略為:上->右->下->左
// map 表示地圖,i j 是指定從地圖的哪個點開始出發,測試的時候我們指定為 (1, 1)
def findWay2(map: Array[Array[Int]], i: Int, j: Int): Boolean = {
if (map(6)(5) == 2) { // 表示路已經通了
return true
} else {
if (map(i)(j) == 0) { // 0表示可以走還沒有走
// 開始遞歸回溯(試探性判斷)
map(i)(j) = 2 // 先假定該點可以走通
if (findWay2(map, i - 1, j)) { // 先向上找,可以走通
return true
} else if (findWay2(map, i, j + 1)) { // 先向上找,不可以走通,則向右找,可以走通
return true
} else if (findWay2(map, i + 1, j)) { // 先上找,不可以走通,則向右找,不可以走通,則向下找,可以走通
return true
} else if (findWay2(map, i, j - 1)) { // 向 上 右 下 不可以走通,則向左走,可以走通
return true
} else { // 都不可以走通
map(i)(j) = 3
return false
}
} else { // map(i)(j) == 1或者2或者3
return false
}
}
}
}
}
輸出結果如下:
1 1 1 1 1 1 1
1 0 0 0 0 0 1
1 0 0 0 0 0 1
1 1 1 0 0 0 1
1 0 0 0 0 0 1
1 0 0 0 0 0 1
1 0 0 0 0 0 1
1 1 1 1 1 1 1
----------策略為:上->右->下->左----------
1 1 1 1 1 1 1
1 2 2 2 2 2 1
1 0 0 0 0 2 1
1 1 1 0 0 2 1
1 0 0 0 0 2 1
1 0 0 0 0 2 1
1 0 0 0 0 2 1
1 1 1 1 1 1 1
19.8 排序 sort
19.8.1 排序的介紹
排序是將一組數據,依指定的順序進行排列的過程,常見的排序:
1) 冒泡排序
2) 選擇排序
3) 插入排序
4) 快速排序
5) 歸並排序
19.8.2 冒泡排序
冒泡排序思想
冒泡排序(Bubble Sorting)的基本思想是:通過對待排序序列從后向前(從下標較大的元素開始),依次比較相鄰元素的排序碼,若發現逆序則交換,使排序碼較小的元素逐漸從后部移向前部(從下標較大的單元移向下標較小的單元),就象水底下的氣泡一樣逐漸向上冒。
因為排序的過程中,各元素不斷接近自己的位置,如果一趟比較下來沒有進行過交換,就說明序列有序,因此要在排序過程中設置一個標志 flag 判斷元素是否進行過交換。從而減少不必要的比較。
冒泡排序的代碼:
package com.atguigu.chapter19.sort
import java.text.SimpleDateFormat
import java.util.Date
object BubbleSortDemo01 {
def main(args: Array[String]): Unit = {
// 數組
// val arr = Array(3, 9, -1, 10, 20)
// 創建一個80000個隨機數據的數組,冒泡排序用時10秒
val random = new util.Random()
val arr = new Array[Int](80000)
for (i <- 0 until 80000) {
arr(i) = random.nextInt(8000000)
}
val dateFormat: SimpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
val now: Date = new Date()
val date = dateFormat.format(now)
println("冒泡排序前")
// println(arr.mkString(" "))
println("冒泡排序前時間 = " + date) // 輸出時間
println("冒泡排序后")
bubbleSort(arr)
// println(arr.mkString(" "))
val now2: Date = new Date()
val date2 = dateFormat.format(now2)
println("冒泡排序后時間 = " + date2) // 輸出時間
}
def bubbleSort(arr: Array[Int]): Unit = {
for (i <- 0 until arr.length - 1) {
for (j <- 0 until arr.length - 1 - i) {
if (arr(j) > arr(j + 1)) {
val temp = arr(j)
arr(j) = arr(j + 1)
arr(j + 1) = temp
}
}
}
}
}
輸出結果如下:
冒泡排序前
冒泡排序前時間 = 2019-04-10 09:32:33
冒泡排序后
冒泡排序后時間 = 2019-04-10 09:32:43
19.8.3 選擇排序
基本介紹
選擇式排序也屬於內部排序法(內存排序),是從排序的數據中,按指定的規則選出某一元素,經過和其他元素重整,再依規定交換位置后達到排序的目的。
選擇排序思想
選擇排序(select sorting)也是一種簡單的排序方法。它的基本思想是:第一次從 R[0]~R[n-1] 中選取最小值,與 R[0] 交換,第二次從 R[1]~R[n-1] 中選取最小值,與 R[1] 交換,第三次從 R[2]~R[n-1] 中選取最小值,與 R[2] 交換,…,第 i 次從 R[i-1]~R[n-1] 中選取最小值,與 R[i-1] 交換,…, 第 n-1 次從 R[n-2]~R[n-1] 中選取最小值,與 R[n-2] 交換,總共通過 n-1 次,得到一個按排序碼從小到大排列的有序序列。
選擇排序思路分析圖
選擇排序的代碼:
package com.atguigu.chapter19.sort
import java.text.SimpleDateFormat
import java.util.Date
object SelectSortDemo01 {
def main(args: Array[String]): Unit = {
// var arr = Array(101, 34, 119, 1)
// 創建一個80000個隨機數據的數組,選擇排序用時3秒
val random = new util.Random()
val arr = new Array[Int](80000)
for (i <- 0 until 80000) {
arr(i) = random.nextInt(8000000)
}
val dateFormat: SimpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
val now: Date = new Date()
val date = dateFormat.format(now)
println("選擇排序前")
// println(arr.mkString(" "))
println("選擇排序前時間 = " + date) // 輸出時間
println("選擇排序后")
selectSort(arr)
// println(arr.mkString(" "))
val now2: Date = new Date()
val date2 = dateFormat.format(now2)
println("選擇排序后時間 = " + date2) // 輸出時間
/*
// 選擇排序演變過程
// 第1輪選擇排序 (101, 34, 119, 1) => (1, 34, 119, 101)
var min = arr(0) // 假定第一個為最小值
var minIndex = 0
// 遍歷
for (j <- 0 + 1 until arr.length) {
if (min > arr(j)) { // 說明 min 不是真的最小值
min = arr(j) // 重置 min
minIndex = j // 重置 minIndex
}
}
// 判斷一下是否需要交換位置(注意:這里沒有交換位置操作,實際是賦值操作,因為新的最小值已經被我們記錄下了,效率更高)
if (minIndex != 0) {
arr(minIndex) = arr(0)
arr(0) = min // 這是賦值操作
}
println("第1輪選擇排序結束,結果是")
println(arr.mkString(" "))
// 第2輪選擇排序 (1, 34, 119, 101) => (1, 34, 119, 101)
min = arr(1)
minIndex = 1
// 遍歷
for (j <- 1 + 1 until arr.length) {
if (min > arr(j)) { // 說明 min 不是真的最小值
min = arr(j) // 重置 min
minIndex = j // 重置 minIndex
}
}
// 判斷一下是否需要交換位置(注意:這里沒有交換位置操作,實際是賦值操作,因為新的最小值已經被我們記錄下了,效率更高)
if (minIndex != 1) {
arr(minIndex) = arr(1)
arr(1) = min // 這是賦值操作
}
println("第2輪選擇排序結束,結果是")
println(arr.mkString(" "))
// 第3輪選擇排序 (1, 34, 119, 101) => (1, 34, 101, 119)
min = arr(2)
minIndex = 2
// 遍歷
for (j <- 2 + 1 until arr.length) {
if (min > arr(j)) { // 說明 min 不是真的最小值
min = arr(j) // 重置 min
minIndex = j // 重置 minIndex
}
}
// 判斷一下是否需要交換位置(注意:這里沒有交換位置操作,實際是賦值操作,因為新的最小值已經被我們記錄下了,效率更高)
if (minIndex != 2) {
arr(minIndex) = arr(2)
arr(2) = min // 這是賦值操作
}
println("第3輪選擇排序結束,結果是")
println(arr.mkString(" "))
*/
/*
// 總結規律
for (i <- 0 until arr.length - 1) {
var min = arr(i) // 假定第一個為最小值
var minIndex = i
// 遍歷
for (j <- i + 1 until arr.length) {
if (min > arr(j)) { // 說明 min 不是真的最小值
min = arr(j) // 重置 min
minIndex = j // 重置 minIndex
}
}
// 判斷一下是否需要交換位置(注意:這里沒有交換位置操作,實際是賦值操作,因為新的最小值已經被我們記錄下了,效率更高)
if (minIndex != i) {
arr(minIndex) = arr(i)
arr(i) = min // 這是賦值操作
}
println(s"選擇排序第 ${i + 1} 輪結束,結果是")
println(arr.mkString(" "))
}
*/
}
def selectSort(arr: Array[Int]): Unit = {
for (i <- 0 until arr.length - 1) {
var min = arr(i) // 假定第一個為最小值
var minIndex = i
// 遍歷
for (j <- i + 1 until arr.length) {
if (min > arr(j)) { // 說明 min 不是真的最小值
min = arr(j) // 重置 min
minIndex = j // 重置 minIndex
}
}
// 判斷一下是否需要交換位置(注意:這里沒有交換位置操作,實際是賦值操作,因為新的最小值已經被我們記錄下了,效率更高)
if (minIndex != i) {
arr(minIndex) = arr(i)
arr(i) = min // 這是賦值操作
}
}
}
}
輸出結果如下:
選擇排序前
選擇排序前時間 = 2019-04-10 10:21:06
選擇排序后
選擇排序后時間 = 2019-04-10 10:21:09
19.8.4 插入排序
基本介紹
插入式排序屬於內部排序法,對於欲排序的元素以插入的方式找尋該元素的適當位置,以達到排序的目的。
插入排序法思想
插入排序(Insertion Sorting)的基本思想是:把 n 個待排序的元素看成為一個有序表和一個無序表,開始時有序表中只包含一個元素,無序表中包含有 n-1 個元素,排序過程中每次從無序表中取出第一個元素,把它的排序碼依次與有序表元素的排序碼進行比較,將它插入到有序表中的適當位置,使之成為新的有序表。
插入排序思路分析圖
插入排序的代碼:
package com.atguigu.chapter19.sort
import java.text.SimpleDateFormat
import java.util.Date
import com.atguigu.chapter19.sort.SelectSortDemo01.selectSort
object InsertSortDemo01 {
def main(args: Array[String]): Unit = {
// var arr = Array(101, 34, 119, 1)
// 創建一個80000個隨機數據的數組,插入排序用時1秒
val random = new util.Random()
val arr = new Array[Int](80000)
for (i <- 0 until 80000) {
arr(i) = random.nextInt(8000000)
}
val dateFormat: SimpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
val now: Date = new Date()
val date = dateFormat.format(now)
println("插入排序前")
// println(arr.mkString(" "))
println("選擇排序前時間 = " + date) // 輸出時間
println("插入排序后")
selectSort(arr)
// println(arr.mkString(" "))
val now2: Date = new Date()
val date2 = dateFormat.format(now2)
println("插入排序后時間 = " + date2) // 輸出時間
/*
// 插入排序演變過程
// 第1輪插入排序 ((101), 34, 119, 1) => ((34, 101), 119, 1)
val insertValue = arr(1) // 將要插入的元素的值
var insertIndex = 1 - 1 // 表示(101)有序表的最后這個元素的索引,即有序表的最大值的索引
while (insertIndex >= 0 && arr(insertIndex) > insertValue) {
arr(insertIndex + 1) = arr(insertIndex) // 插入的位置的意思是:插入有序表中最后一個元素的下一個位置,插入的是自己
insertIndex -= 1 // insertIndex = -1
}
// 退出 while 循環或者不進入 while 循環,表示要插入的位置找到了
arr(insertIndex + 1) = insertValue // 插入的位置的意思是:插入有序表中最后一個元素的下一個位置,插入的是要插入的值
println("第1輪插入排序結束,結果是")
println(arr.mkString(" "))
// 第2輪插入排序 ((34, 101), 119, 1) => ((34, 101, 119), 1)
insertValue = arr(2) // 將要插入的元素的值
insertIndex = 2 - 1 // 表示(34, 101)有序表的最后這個元素的索引,即有序表的最大值的索引
while (insertIndex >= 0 && arr(insertIndex) > insertValue) {
arr(insertIndex + 1) = arr(insertIndex) // 插入的位置的意思是:插入有序表中最后一個元素的下一個位置,插入的是自己
insertIndex -= 1 // insertIndex = -1
}
// 退出 while 循環或者不進入 while 循環,表示要插入的位置找到了
arr(insertIndex + 1) = insertValue // 插入的位置的意思是:插入有序表中最后一個元素的下一個位置,插入的是要插入的值
println("第2輪插入排序結束,結果是")
println(arr.mkString(" "))
// 第3輪插入排序 ((34, 101, 119), 1) => (1, 34, 101, 119)
insertValue = arr(3) // 將要插入的元素的值
insertIndex = 3 - 1 // 表示(34, 101, 119)有序表的最后這個元素的索引,即有序表的最大值的索引
while (insertIndex >= 0 && arr(insertIndex) > insertValue) {
arr(insertIndex + 1) = arr(insertIndex) // 插入的位置的意思是:插入有序表中最后一個元素的下一個位置,插入的是自己
insertIndex -= 1 // insertIndex = -1
}
// 退出 while 循環或者不進入 while 循環,表示要插入的位置找到了
arr(insertIndex + 1) = insertValue // 插入的位置的意思是:插入有序表中最后一個元素的下一個位置,插入的是要插入的值
println("第3輪插入排序結束,結果是")
println(arr.mkString(" "))
// 總結規律
for (i <- 1 until arr.length) {
val insertValue = arr(i) // 將要插入的元素的值
var insertIndex = i - 1 // 表示有序表的最后這個元素的索引,即有序表的最大值的索引
while (insertIndex >= 0 && arr(insertIndex) > insertValue) {
arr(insertIndex + 1) = arr(insertIndex) // 插入的位置的意思是:插入有序表中最后一個元素的下一個位置,插入的是自己
insertIndex -= 1 // insertIndex = -1
}
// 退出 while 循環或者不進入 while 循環,表示要插入的位置找到了
arr(insertIndex + 1) = insertValue // 插入的位置的意思是:插入有序表中最后一個元素的下一個位置,插入的是要插入的值
println(s"第${i}輪插入排序結束,結果是")
println(arr.mkString(" "))
}
*/
}
def insertSort(arr: Array[Int]): Unit = {
for (i <- 1 until arr.length) {
val insertValue = arr(i) // 將要插入的元素的值
var insertIndex = i - 1 // 表示有序表的最后這個元素的索引,即有序表的最大值的索引
while (insertIndex >= 0 && arr(insertIndex) > insertValue) {
arr(insertIndex + 1) = arr(insertIndex) // 插入的位置的意思是:插入有序表中最后一個元素的下一個位置,插入的是自己
insertIndex -= 1 // insertIndex = -1
}
// 退出 while 循環或者不進入 while 循環,表示要插入的位置找到了
arr(insertIndex + 1) = insertValue // 插入的位置的意思是:插入有序表中最后一個元素的下一個位置,插入的是要插入的值
}
}
}
輸出結果如下:
插入排序前
選擇排序前時間 = 2019-04-10 11:27:55
插入排序后
插入排序后時間 = 2019-04-10 11:27:56
19.8.5 快速排序
基本介紹
快速排序(Quicksort)是對冒泡排序的一種改進。
插入排序法思想
基本思想是:通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,然后再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。
快速排序示意圖
快速排序的代碼:
package com.atguigu.chapter19.sort
import java.text.SimpleDateFormat
import java.util.Date
import util.control.Breaks._
object QuickSortDemo01 {
def main(args: Array[String]): Unit = {
// var arr = Array[Int](-9, 78, 0, 23, -567, 70)
// println("快速排序前")
// println(arr.mkString(" "))
// println("快速排序后")
// quickSort(0, arr.length - 1, arr)
// println(arr.mkString(" "))
// 如果取消左右遞歸,結果是 -9 -567 0 23 78 70
// 如果取消右遞歸,結果是 -567 -9 0 23 78 70
// 如果取消左遞歸,結果是 -9 -567 0 23 70 78
// 如果正常,結果是 -567 -9 0 23 70 78
// 創建一個8000 0000個隨機數據的數組,快速排序用時12秒(八千萬個數據),太快了!!!
val random = new util.Random()
val arr = new Array[Int](80000000)
for (i <- 0 until 80000000) {
arr(i) = random.nextInt(800000000)
}
val dateFormat: SimpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
val now: Date = new Date()
val date = dateFormat.format(now)
println("快速排序前")
// println(arr.mkString(" "))
println("快速排序前時間 = " + date) // 輸出時間
println("快速排序后")
quickSort(0, arr.length - 1, arr)
// println(arr.mkString(" "))
val now2: Date = new Date()
val date2 = dateFormat.format(now2)
println("快速排序后時間 = " + date2) // 輸出時間
}
// left: 從數組的左邊的索引 0
// right: 從數組的右邊的索引 arr.length - 1
// arr: 進行排序的數組
def quickSort(left: Int, right: Int, arr: Array[Int]): Unit = {
var l = left
var r = right
val pivot = arr((left + right) / 2)
breakable {
// while 語句的作用就是把比 pivot 小的數放到左邊,比 pivot 大的數放到右邊
while (l < r) {
while (arr(l) < pivot) { // 從左邊找一個比 pivot 大的值
l += 1
}
while (arr(r) > pivot) { // 從右邊找一個比 pivot 小的值
r -= 1
}
if (l >= r) { // 說明本次交換結束,退出本次 while
break()
}
// 交換二者位置
val temp = arr(l)
arr(l) = arr(r)
arr(r) = temp
// 二者已經交換后再進行的判斷,即 arr(l) 表示的是 右邊
if (arr(l) == pivot) { // 如果 從右邊找一個與 pivot 相等的值,則不用交換,繼續進行右邊下一個,提高效率
r -= 1
}
// 二者已經交換后再進行的判斷,即 arr(r) 表示的是 左邊
if (arr(r) == pivot) { // 如果 從左邊找一個與 pivot 相等的值,則不用交換,繼續進行左邊下一個,提高效率
l += 1
}
}
}
// 提高效率
if (l == r) {
l += 1 // 為向右遞歸做准備
r -= 1 // 為向左遞歸做准備
}
if (left < r) { // 向左遞歸
quickSort(left, r, arr)
}
if (right > l) { // 向右遞歸
quickSort(l, right, arr)
}
}
}
輸出結果如下:
快速排序前
快速排序前時間 = 2019-04-10 13:25:56
快速排序后
快速排序后時間 = 2019-04-10 13:26:06
快速排序代碼詳細圖解
19.8.6 歸並排序
基本介紹
歸並排序(MERGE-SORT)是利用歸並的思想實現的排序方法,該算法采用經典的分治(divide-and-conquer)策略(分治法將問題分(divide)成一些小的問題然后遞歸求解,而治(conquer)的階段則將分的階段得到的各答案"修補"在一起,即分而治之)。
歸並排序思想示意圖1-基本思想
可以看到這種結構很像一棵完全二叉樹,本文的歸並排序我們采用遞歸去實現(也可采用迭代的方式去實現)。分的階段可以理解為就是遞歸拆分子序列的過程。
歸並排序思想示意圖2-合並相鄰有序子序列
再來看看治的階段,我們需要將兩個已經有序的子序列合並成一個有序序列,比如上圖中的最后一次合並,要將 [4,5,7,8] 和 [1,2,3,6] 兩個已經有序的子序列,合並為最終序列 [1,2,3,4,5,6,7,8],來看下實現步驟:
歸並排序的代碼:
package com.atguigu.chapter19.sort
import java.text.SimpleDateFormat
import java.util.Date
object MergeSortDemo01 {
def main(args: Array[String]): Unit = {
// val arr = Array(7, 6, 5, 4, 3, 2, 1)
// val temp = new Array[Int](arr.length)
// println("歸並排序前")
// println(arr.mkString(" "))
// println("歸並排序后")
// mergeSort(arr, 0, arr.length - 1, temp)
// println(arr.mkString(" "))
// 創建一個8000 0000個隨機數據的數組,歸並排序用時12秒(八千萬個數據),太快了!!!
val random = new util.Random()
val arr = new Array[Int](80000000)
val temp = new Array[Int](arr.length)
for (i <- 0 until 80000000) {
arr(i) = random.nextInt(800000000)
}
val dateFormat: SimpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
val now: Date = new Date()
val date = dateFormat.format(now)
println("歸並排序前")
// println(arr.mkString(" "))
println("歸並排序前時間 = " + date) // 輸出時間
println("歸並排序后")
mergeSort(arr, 0, arr.length - 1, temp)
// println(arr.mkString(" "))
val now2: Date = new Date()
val date2 = dateFormat.format(now2)
println("歸並排序后時間 = " + date2) // 輸出時間
}
// 歸並排序
// arr 待排序的數組
// left 數組的左邊的索引 0
// right 數組的右邊邊的索引 arr.length - 1
// temp 臨時數組,事先開辟好的,大小與 arr 一樣
def mergeSort(arr: Array[Int], left: Int, right: Int, temp: Array[Int]): Unit = {
// 拆分操作
if (left < right) {
val mid = (left + right) / 2
mergeSort(arr, left, mid, temp) // 遞歸拆分左邊數組成有序列表
mergeSort(arr, mid + 1, right, temp) // 遞歸拆分右邊數組成有序列表
merge(arr, left, mid, right, temp) // 合並
}
}
// 合並操作
def merge(arr: Array[Int], left: Int, mid: Int, right: Int, temp: Array[Int]) {
var i = left // 輔助指針
var j = mid + 1 // 輔助指針
var t = 0 // 表示臨時數組的第一個元素的索引
while (i <= mid && j <= right) {
if (arr(i) <= arr(j)) { // 當前左邊的有序列表的值小於當前右邊有序列表的值
temp(t) = arr(i) // 把當前左邊的有序列表的值依次賦值給臨時數組
t += 1 // 臨時數組的索引右移一位
i += 1 // 左邊有序列表的索引右移一位
} else {
temp(t) = arr(j) // 把當前右邊的有序列表的值依次賦值給臨時數組
t += 1 // 臨時數組的索引右移一位
j += 1 // 右邊有序列表的索引右移一位
}
}
while (i <= mid) { // 說明左邊有序列表還有數據,就把當前左邊的有序列表的值依次賦值給臨時數組
temp(t) = arr(i)
t += 1
i += 1
}
while (j <= right) { // 說明右邊有序列表還有數據,就把當前右邊的有序列表的值依次賦值給臨時數組
temp(t) = arr(j)
t += 1
j += 1
}
// 下面代碼時完成將本次的臨時數組 temp 的數據拷貝到原始數組 arr 中
t = 0 // 歸位到臨時數組的第一個元素的索引
var tempLeft = left // 輔助指針
while (tempLeft <= right) { // 將臨時數組中的數據依次拷貝至原數組中去
arr(tempLeft) = temp(t)
t += 1
tempLeft += 1
}
}
}
輸出結果如下:
歸並排序前
歸並排序前時間 = 2019-04-10 17:40:46
歸並排序后
歸並排序后時間 = 2019-04-10 17:40:58
19.9 查找
18.9.1 介紹
在 java 中,我們常用的查找有兩種:
1、順序(線性)查找
2、二分查找
19.9.2 線性查找
有一個數列:{1, 8, 10, 89, 1000, 1234} ,判斷數列中是否包含此名稱。
要求: 如果找到了,就提示找到,並給出下標值。
19.9.3 二分查找
請對一個有序數組進行二分查找 {1, 8, 10, 89, 1000, 1234},輸入一個數看看該數組是否存在此數,並且求出下標,如果沒有就提示"沒有這個數"。
課后思考題:{1, 8, 10, 89, 1000, 1000, 1234} 當一個有序數組中,有多個相同的數值時,如何將所有的數值都查找到,比如:這里的 1000
二分查找 + 二分查找所有相同的值 的代碼實現
package com.atguigu.chapter19.search
import scala.collection.mutable.ArrayBuffer
import util.control.Breaks._
object BinarySearchDemo01 {
def main(args: Array[String]): Unit = {
val arr = Array(1, 8, 10, 89, 1000, 1000, 1000, 1234)
// 測試:二分查找
val index = binarySearch(arr, 0, arr.length - 1, 1000)
if (index != -1) {
println("找到,索引為 = " + index)
} else {
println("沒有找到")
}
println("--------------------")
// 測試:二分查找所有相同的值
val resArr = binarySearch2(arr, 0, arr.length - 1, 1000)
if (resArr.length != 0) {
for (index <- resArr) {
println("找到,索引分別 = " + index)
}
} else {
println("沒有找到")
}
}
// 二分查找
// 1. 先找到中間值
// 2. 然后將中間值和查找的值進行比較
// 2.1 相等,找到,返回索引
// 2.2 查找值 < 中間值,向左進行遞歸查找
// 2.3 查找值 > 中間值,向右進行遞歸查找
// 如果存在該值,就返回對應的索引,否則返回 -1
def binarySearch(arr: Array[Int], left: Int, right: Int, findValue: Int): Int = {
val midIndex = (left + right) / 2
val midValue = arr(midIndex)
// 找不到的情況判斷
if (left > right) {
return -1
}
if (findValue < midValue) {
binarySearch(arr, left, midIndex - 1, findValue)
} else if (findValue > midValue) {
binarySearch(arr, midIndex + 1, right, findValue)
} else {
return midIndex
}
}
// 二分查找所有相同的值
// 1. 先找到中間值
// 2. 然后將中間值和查找的值進行比較
// 2.1 相等,找到,返回索引
// 2.2 查找值 < 中間值,向左進行遞歸查找
// 2.3 查找值 > 中間值,向右進行遞歸查找
// 如果存在該值,就返回對應的索引,否則返回 -1
// 1.返回個結果是一個可變數組 ArrayBuffer
// 2.在找到結果時,分別向左邊掃描和向右邊掃描
// 3.又找到結果后,就加入到 ArrayBuffer,在分別向左邊掃描和向右邊掃描......
def binarySearch2(arr: Array[Int], left: Int, right: Int, findValue: Int): ArrayBuffer[Int] = {
val midIndex = (left + right) / 2
val midValue = arr(midIndex)
// 如果找不到,返回 -1
if (left > right) {
return ArrayBuffer() // 返回一個空的可變數組,之后可以通過數組長度進行判斷
}
// 如果找到,返回對應的索引
if (findValue < midValue) {
binarySearch2(arr, left, midIndex - 1, findValue)
} else if (findValue > midValue) {
binarySearch2(arr, midIndex + 1, right, findValue)
} else {
// 定義一個可變數組
val resArr = ArrayBuffer[Int]()
// 向左掃描
var temp = midIndex - 1 // 輔助指針
breakable {
while (true) {
if (temp < 0 || arr(temp) != findValue) {
break()
}
if (arr(temp) == findValue) {
resArr.append(temp)
}
temp -= 1 // 找到,移動指針,繼續向左掃描
}
}
// 將中間這個已經找到的索引加入到可變數組
resArr.append(midIndex) // 注意:該句代碼放的位置不同,可能導致找到的索引輸出的結果順序不同
// 向右掃描
temp = midIndex + 1 // 輔助指針
breakable {
while (true) {
if (temp > right || arr(temp) != findValue) {
break()
}
if (arr(temp) == findValue) {
resArr.append(temp)
}
temp += 1 // 找到,移動指針,繼續向右掃描
}
}
return resArr
}
}
}
輸出結果如下:
找到,索引為 = 5
--------------------
找到,索引分別 = 4
找到,索引分別 = 5
找到,索引分別 = 6
19.10 哈希表(散列表)
19.10.1 看一個實際需求
google 公司的一個上機題:
有一個公司,當有新的員工來報道時,要求將該員工的信息加入(id,性別,年齡,住址…),當輸入該員工的 id 時,要求查找到該員工的所有信息。
要求:不使用數據庫,盡量節省內存,速度越快越好 => 哈希表(散列)
19.10.2 哈希表的基本介紹
散列表(Hash table,也叫哈希表),是根據關鍵碼值(key value)而直接進行訪問的數據結構。也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做散列函數,存放記錄的數組叫做散列表。
自定義緩存
19.10.3 應用實例
google 公司的一個上機題:
有一個公司,當有新的員工來報道時,要求將該員工的信息加入(id,性別,年齡,住址…),當輸入該員工的 id 時,要求查找到該員工的所有信息。
要求:
1、不使用數據庫,盡量節省內存,速度越快越好 => 哈希表(散列)
2、使用鏈表來實現哈希表,該鏈表不帶表頭。[即: 鏈表的第一個結點就存放雇員信息]
3、添加時,保證按照 id 從低到高插入。
思路分析示意圖
[思考:如果 id 不是從低到高插入,但要求各條鏈表仍是從低到高,怎么解決?以及插入鏈表中的數據不能重復,如何解決?] 代碼如下
代碼實現[增刪改查(顯示所有員工,按 id 查詢)]
package com.atguigu.chapter19.hashtab
import scala.io.StdIn
import util.control.Breaks._
object HashTabDemo01 {
def main(args: Array[String]): Unit = {
// 創建 HashTab
val hashTab = new HashTab(7)
// 寫一個簡單地菜單
var key = ""
while (true) {
println("add:添加雇員 ")
println("list:顯示雇員")
println("find:查找雇員")
println("exit:退出系統")
key = StdIn.readLine()
key match {
case "add" => {
print("請輸入id:")
val id = StdIn.readInt()
print("請輸入名字:")
val name = StdIn.readLine()
val emp = new Emp(id, name)
hashTab.add2(emp)
}
case "list" => {
hashTab.list()
}
case "find" => {
print("請輸入id:")
val id = StdIn.readInt()
hashTab.findEmpById(id)
}
case "exit" => {
System.exit(0)
}
}
}
}
}
// 創建 HashTab,用於雇員鏈表的增刪改查+決定雇員應該添加到哪一條具體的雇員鏈表上 等等
class HashTab(val size: Int) { // 暫時默認 size = 7,注意:size 是只讀屬性
val empLinkedListArr: Array[EmpLinkedList] = new Array[EmpLinkedList](size)
// 初始化雇員鏈表數組的各個元素
for (i <- 0 until size) {
empLinkedListArr(i) = new EmpLinkedList
}
// 散列函數(該函數視具體情況而定)
// 決定雇員應該添加到哪一條具體的雇員鏈表上
def hashFun(id: Int): Int = {
id % size
}
// 向雇員鏈表上添加雇員
def add2(emp: Emp): Unit = {
val empLinkedListNo = hashFun(emp.id) // Array 數組的索引,即具體哪一天鏈表
// 判斷要添加的雇員 id 是否存在,如果存在,則直接返回並彈出信息;如果不存在,則添加
if (this.empLinkedListArr(empLinkedListNo).findEmpById(emp.id) != null) {
printf("要添加的雇員 id=%d 已存在,請重新添加\n", emp.id)
return
} else {
this.empLinkedListArr(empLinkedListNo).add2(emp)
}
}
// 遍歷整個哈希表
def list(): Unit = {
for (i <- 0 until size) {
empLinkedListArr(i).list(i)
}
}
// 查找雇員
def findEmpById(id: Int): Unit = {
val empLinkedListNo = hashFun(id) // Array 數組的索引,即具體哪一天鏈表
val emp = this.empLinkedListArr(empLinkedListNo).findEmpById(id)
if (emp != null) {
printf(s"在第 ${empLinkedListNo} 條雇員鏈表上找到 id=%d name=%s 的雇員\n", id, emp.name)
} else {
printf("沒有找到id為 %d 的雇員\n", id)
}
}
}
// 創建 EmpLinkedList,用於雇員的增刪改查
class EmpLinkedList {
// 定義頭指針,注意:這里的 head 直接指向一個雇員
var head: Emp = null
// 添加雇員的方法一
// 找到鏈表的尾部加入即可
// 假定添加雇員的 id 是自增的,即雇員分配的 id 總是從小到大
def add(emp: Emp): Unit = {
// 對於第一個雇員
if (head == null) {
head = emp // head 直接指向第一個雇員
return
}
// 定義一個輔助指針
var temp = head
// 找到鏈表的尾部
breakable {
while (true) {
if (temp.next == null) { // 說明已到該鏈表的尾部
break()
}
temp = temp.next // 后移指針
}
}
// 該鏈表的尾部指向新加入的雇員
temp.next = emp
}
// 添加雇員的方法二
// 在添加雇員的時候,根據雇員的id將雇員插入指定的位置(如果該雇員的 id 已存在,則添加失敗,並給出提示)
def add2(emp: Emp): Unit = {
// 對於第一個雇員
if (head == null) {
head = emp // head 直接指向第一個雇員,由於我們的頭結點也使用,所以判斷雇員的 id 是否存在的操作,我們放在了調用添加雇員方法之前。
return
}
// 走到這一步,說明鏈表中至少有一個雇員了
// 定義一個輔助指針
var temp = head
var flag = false
breakable {
while (true) {
if (temp.next == null) { // 說明已到該鏈表的尾部
break()
}
if (emp.id < temp.id) { // 執行到這一步,說明鏈表中已經有兩個以上(包括兩個)雇員
flag = true
break()
}
if (emp.id < temp.next.id) { // 說明位置找到,當前這個節點 Emp 應加入到節點 temp 的后面 和節點 temp.next 的前面
break()
}
temp = temp.next // 后移指針
}
}
if (flag) { // 當鏈表中已經有兩個以上(包括兩個)雇員,新的雇員添加至鏈表頭的操作
head = emp
emp.next = temp
} else {
if (emp.id < temp.id) { // 當鏈表中只有一個雇員,新的雇員添加至鏈表頭的操作
head = emp
emp.next = temp
} else { // 當鏈表中已經有兩個以上(包括兩個)雇員,,新的雇員添加至鏈表中的操作
// 添加雇員,注意添加的順序
emp.next = temp.next
temp.next = emp
}
}
}
// 遍歷雇員鏈表
def list(i: Int): Unit = {
if (head == null) {
println(s"第 ${i} 條雇員鏈表的數據為空")
return
}
print(s"第 ${i} 條雇員鏈表的數據為:")
// 定義一個輔助指針
var temp = head
breakable {
while (true) {
if (temp == null) {
break()
}
// 輸出雇員信息
printf("=> id=%d name=%s\t", temp.id, temp.name)
temp = temp.next // 后移指針
}
}
println()
}
// 查找雇員,找到返回 Emp,找不到返回 null
def findEmpById(id: Int): Emp = {
// 遍歷
if (head == null) {
return null
}
// 定義一個輔助指針
var temp = head
breakable {
while (true) {
if (temp == null) {
break()
}
if (temp.id == id) {
break()
}
temp = temp.next
}
}
return temp
}
}
// 創建雇員類
class Emp(eId: Int, eName: String) {
val id = eId
var name = eName
var next: Emp = null
}
核心代碼截圖如下:
19.11 二叉樹
19.11.1 為什么需要樹這種數據結構
1、數組存儲方式的分析
優點:通過索引的方式訪問元素,速度快。對於有序數組,還可使用二分查找提高檢索速度。
缺點:如果要檢索具體某個值,或者插入值(按一定順序)會整體移動,效率較低。
2、鏈式存儲方式的分析
優點:在一定程度上對數組存儲方式有優化(比如:插入一個數值節點,只需要將插入的節點,鏈接到鏈表中即可)。
缺點:在進行檢索時,效率仍然較低,比如(檢索某個值,需要從頭節點開始遍歷)。
3、樹存儲方式的分析
能提高數據存儲、讀取的效率,比如:利用二叉排序樹(Binary Sort Tree),既可以保證數據的檢索速度,同時也可以保證數據的插入、刪除、修改的速度。
19.11.2 二叉樹的示意圖
19.11.3 二叉樹的概念
1、樹有很多種,每個節點最多只能有兩個子節點的一種形式稱為二叉樹。
2、二叉樹的子節點分為左節點和右節點。
3、如果該二叉樹的
所有葉子節點都在最后一層,並且
結點總數 = 2^n - 1,n 為層數,則我們稱為
滿二叉樹。
4、如果該二叉樹的所有葉子節點都在最后一層或者倒數第二層,而且最后一層的葉子節點在左邊連續,倒數第二層的葉子節點在右邊連續,我們稱為
完全二叉樹。
19.11.4 二叉樹遍歷的說明
使用前序、中序和后序對下面的二叉樹進行遍歷,對各種遍歷方式的說明:
前序遍歷:先輸出父節點,再遍歷左子樹和右子樹。
中序遍歷:先遍歷左子樹,再輸出父節點,再遍歷右子樹。
后序遍歷:先遍歷左子樹,再遍歷右子樹,最后輸出父節點。
小結:看輸出父節點的順序,就確定是前序、中序還是后序。
19.11.5 二叉樹遍歷應用實例(前序、中序、后序)
示例代碼如下:
package com.atguigu.chapter19.binarytree
object BinaryTreeDemo01 {
def main(args: Array[String]): Unit = {
// 先使用簡單的方法:直接手動關聯
val heroNode1 = new HeroNode(1, "宋江")
val heroNode2 = new HeroNode(2, "盧俊義")
val heroNode3 = new HeroNode(3, "吳用")
val heroNode4 = new HeroNode(4, "入雲龍")
val heroNode5 = new HeroNode(5, "關勝")
heroNode1.left = heroNode2
heroNode1.right = heroNode3
heroNode3.left = heroNode5
heroNode3.right = heroNode4
val binaryTree = new BinaryTree
binaryTree.root = heroNode1
println("-----前序遍歷的結果-----")
binaryTree.preOrder()
println("-----中序遍歷的結果-----")
binaryTree.infixOrder()
println("-----后序遍歷的結果-----")
binaryTree.postOrder()
}
}
// 定義管理英雄節點的二叉樹
class BinaryTree {
var root: HeroNode = null
// 前序遍歷
def preOrder(): Unit = {
if (root != null) {
root.preOrder()
} else {
println("當前二叉樹為空,不能遍歷")
}
}
// 中序遍歷
def infixOrder(): Unit = {
if (root != null) {
root.infixOrder()
} else {
println("當前二叉樹為空,不能遍歷")
}
}
// 后序遍歷
def postOrder(): Unit = {
if (root != null) {
root.postOrder()
} else {
println("當前二叉樹為空,不能遍歷")
}
}
}
// 定義英雄節點
class HeroNode(hNo: Int, hName: String) {
val no = hNo
var name = hName
var left: HeroNode = null
var right: HeroNode = null
// 前序遍歷:先輸出父節點,再遍歷左子樹和右子樹。
def preOrder(): Unit = {
// 先輸出當前節點信息
printf("節點信息 no=%d name=%s \n", no, name)
// 向左遞歸輸出左子樹
if (this.left != null) {
this.left.preOrder()
}
// 向右遞歸輸出右子樹
if (this.right != null) {
this.right.preOrder()
}
}
// 中序遍歷:先遍歷左子樹,再輸出父節點,再遍歷右子樹。
def infixOrder(): Unit = {
// 向左遞歸輸出左子樹
if (this.left != null) {
this.left.infixOrder()
}
// 輸出當前節點信息
printf("節點信息 no=%d name=%s \n", no, name)
// 向右遞歸輸出右子樹
if (this.right != null) {
this.right.infixOrder()
}
}
// 后序遍歷:先遍歷左子樹,再遍歷右子樹,最后輸出父節點。
def postOrder(): Unit = {
// 向左遞歸輸出左子樹
if (this.left != null) {
this.left.postOrder()
}
// 向右遞歸輸出右子樹
if (this.right != null) {
this.right.postOrder()
}
// 輸出當前節點信息
printf("節點信息 no=%d name=%s \n", no, name)
}
}
輸出結果如下:
-----前序遍歷的結果-----
節點信息 no=1 name=宋江
節點信息 no=2 name=盧俊義
節點信息 no=3 name=吳用
節點信息 no=5 name=關勝
節點信息 no=4 name=入雲龍
-----中序遍歷的結果-----
節點信息 no=2 name=盧俊義
節點信息 no=1 name=宋江
節點信息 no=5 name=關勝
節點信息 no=3 name=吳用
節點信息 no=4 name=入雲龍
-----后序遍歷的結果-----
節點信息 no=2 name=盧俊義
節點信息 no=5 name=關勝
節點信息 no=4 name=入雲龍
節點信息 no=3 name=吳用
節點信息 no=1 name=宋江
19.11.6 二叉樹-查找指定節點
要求
1、請編寫前序查找、中序查找和后序查找的方法。
2、並分別使用三種查找方式,查找 hNo = 5 的節點
3、並分析各種查找方式,分別比較了多少
4、代碼實現和思路分析
示例代碼如下:
package com.atguigu.chapter19.binarytree
object BinaryTreeDemo01 {
def main(args: Array[String]): Unit = {
// 先使用簡單的方法:直接手動關聯
val heroNode1 = new HeroNode(1, "宋江")
val heroNode2 = new HeroNode(2, "盧俊義")
val heroNode3 = new HeroNode(3, "吳用")
val heroNode4 = new HeroNode(4, "入雲龍")
val heroNode5 = new HeroNode(5, "關勝")
heroNode1.left = heroNode2
heroNode1.right = heroNode3
heroNode3.left = heroNode5
heroNode3.right = heroNode4
val binaryTree = new BinaryTree
binaryTree.root = heroNode1
println("-----前序遍歷的結果-----")
binaryTree.preOrder()
println("-----中序遍歷的結果-----")
binaryTree.infixOrder()
println("-----后序遍歷的結果-----")
binaryTree.postOrder()
println()
println("-----前序查找的結果-----")
val temp = binaryTree.preOrderSearch(5)
if (temp != null) {
printf("找到,該節點的信息是 no=%d name=%s\n", temp.no, temp.name)
} else {
println("沒有找到該節點")
}
println()
println("-----中序查找的結果-----")
val temp2 = binaryTree.infixOrderSearch(5)
if (temp2 != null) {
printf("找到,該節點的信息是 no=%d name=%s\n", temp2.no, temp2.name)
} else {
println("沒有找到該節點")
}
println()
println("-----后序查找的結果-----")
val temp3 = binaryTree.postOrderSearch(5)
if (temp3 != null) {
printf("找到,該節點的信息是 no=%d name=%s\n", temp3.no, temp3.name)
} else {
println("沒有找到該節點")
}
}
}
// 定義管理英雄節點的二叉樹
class BinaryTree {
var root: HeroNode = null
// 前序遍歷
def preOrder(): Unit = {
if (root != null) {
root.preOrder()
} else {
println("當前二叉樹為空,不能遍歷")
}
}
// 中序遍歷
def infixOrder(): Unit = {
if (root != null) {
root.infixOrder()
} else {
println("當前二叉樹為空,不能遍歷")
}
}
// 后序遍歷
def postOrder(): Unit = {
if (root != null) {
root.postOrder()
} else {
println("當前二叉樹為空,不能遍歷")
}
}
// 前序查找
def preOrderSearch(no: Int): HeroNode = {
if (root != null) {
return root.preOrderSearch(no)
} else {
return null // 二叉樹為空
}
}
// 中序查找
def infixOrderSearch(no: Int): HeroNode = {
if (root != null) {
return root.infixOrderSearch(no)
} else {
return null // 二叉樹為空
}
}
// 后序查找
def postOrderSearch(no: Int): HeroNode = {
if (root != null) {
return root.postOrderSearch(no)
} else {
return null // 二叉樹為空
}
}
}
// 定義英雄節點
class HeroNode(hNo: Int, hName: String) {
val no = hNo
var name = hName
var left: HeroNode = null
var right: HeroNode = null
// 前序遍歷:先輸出父節點,再遍歷左子樹和右子樹。
def preOrder(): Unit = {
// 先輸出當前節點信息
printf("節點信息 no=%d name=%s \n", no, name)
// 向左遞歸輸出左子樹
if (this.left != null) {
this.left.preOrder()
}
// 向右遞歸輸出右子樹
if (this.right != null) {
this.right.preOrder()
}
}
// 中序遍歷:先遍歷左子樹,再輸出父節點,再遍歷右子樹。
def infixOrder(): Unit = {
// 向左遞歸輸出左子樹
if (this.left != null) {
this.left.infixOrder()
}
// 輸出當前節點信息
printf("節點信息 no=%d name=%s \n", no, name)
// 向右遞歸輸出右子樹
if (this.right != null) {
this.right.infixOrder()
}
}
// 后序遍歷:先遍歷左子樹,再遍歷右子樹,最后輸出父節點。
def postOrder(): Unit = {
// 向左遞歸輸出左子樹
if (this.left != null) {
this.left.postOrder()
}
// 向右遞歸輸出右子樹
if (this.right != null) {
this.right.postOrder()
}
// 輸出當前節點信息
printf("節點信息 no=%d name=%s \n", no, name)
}
// 前序查找
def preOrderSearch(no: Int): HeroNode = {
println("-----前序查找次數標記-----")
// 先比較當前節點,如果是,就返回當前節點的值,如果不是,就向左子樹遞歸查找
if (no == this.no) {
return this
}
// 向左遞歸查找
var temp: HeroNode = null // 輔助指針
// 如果左子樹不為空,才進行遞歸查找
if (this.left != null) {
temp = this.left.preOrderSearch(no)
}
if (temp != null) {
return temp
}
// 向右遞歸查找
// 如果右子樹不為空,才進行遞歸查找
if (this.right != null) {
temp = this.right.preOrderSearch(no)
}
if (temp != null) {
return temp
}
return temp
}
// 中序查找
def infixOrderSearch(no: Int): HeroNode = {
// 先向左遞歸查找
var temp: HeroNode = null // 輔助指針
// 如果左子樹不為空,才進行遞歸查找
if (this.left != null) {
temp = this.left.infixOrderSearch(no)
}
if (temp != null) {
return temp
}
println("-----中序查找次數標記-----")
// 再比較當前節點,如果是,就返回當前節點的值,如果不是,就向左子樹遞歸查找
if (no == this.no) {
return this
}
// 最后向右遞歸查找
// 如果右子樹不為空,才進行遞歸查找
if (this.right != null) {
temp = this.right.infixOrderSearch(no)
}
if (temp != null) {
return temp
}
return temp
}
// 后序查找
def postOrderSearch(no: Int): HeroNode = {
// 先向左遞歸查找
var temp: HeroNode = null // 輔助指針
// 如果左子樹不為空,才進行遞歸查找
if (this.left != null) {
temp = this.left.postOrderSearch(no)
}
if (temp != null) {
return temp
}
// 再向右遞歸查找
// 如果右子樹不為空,才進行遞歸查找
if (this.right != null) {
temp = this.right.postOrderSearch(no)
}
if (temp != null) {
return temp
}
println("-----后序查找次數標記-----")
// 最后比較當前節點,如果是,就返回當前節點的值,如果不是,就向左子樹遞歸查找
if (no == this.no) {
return this
}
return temp
}
}
輸出結果如下:
-----前序遍歷的結果-----
節點信息 no=1 name=宋江
節點信息 no=2 name=盧俊義
節點信息 no=3 name=吳用
節點信息 no=5 name=關勝
節點信息 no=4 name=入雲龍
-----中序遍歷的結果-----
節點信息 no=2 name=盧俊義
節點信息 no=1 name=宋江
節點信息 no=5 name=關勝
節點信息 no=3 name=吳用
節點信息 no=4 name=入雲龍
-----后序遍歷的結果-----
節點信息 no=2 name=盧俊義
節點信息 no=5 name=關勝
節點信息 no=4 name=入雲龍
節點信息 no=3 name=吳用
節點信息 no=1 name=宋江
-----前序查找的結果-----
-----前序查找次數標記-----
-----前序查找次數標記-----
-----前序查找次數標記-----
-----前序查找次數標記-----
找到,該節點的信息是 no=5 name=關勝
-----中序查找的結果-----
-----中序查找次數標記-----
-----中序查找次數標記-----
-----中序查找次數標記-----
找到,該節點的信息是 no=5 name=關勝
-----后序查找的結果-----
-----后序查找次數標記-----
-----后序查找次數標記-----
找到,該節點的信息是 no=5 name=關勝
19.11.7 二叉樹-刪除節點
要求
1、如果刪除的節點是葉子節點,則刪除該節點
2、如果刪除的節點是非葉子節點,則刪除該子樹
3、測試,刪除掉 5 號葉子節點 和 3 號子樹。
4、代碼,思路在代碼中。
核心代碼示例如下:
println("-----測試刪除節點-------")
binaryTree.delNode(1)
println("-----刪除后前序遍歷---------")
binaryTree.preOrder()
-----------------------------------------------------
// 定義管理英雄節點的二叉樹
class BinaryTree {
var root: HeroNode = null
// 刪除節點
def delNode(no: Int): Unit = {
if (root != null) {
// 先判斷下 root 節點是否是要刪除的節點
if (root.no == no) {
root = null
}
root.delNode(no)
}
}
}
-----------------------------------------------------
// 定義英雄節點
class HeroNode(hNo: Int, hName: String) {
val no = hNo
var name = hName
var left: HeroNode = null
var right: HeroNode = null
// 刪除節點
// 刪除節點的規則一
// (1)如果刪除的節點是葉子節點,則刪除該節點
// (2)如果刪除的節點是非葉子節點,則刪除該子樹
def delNode(no: Int): Unit = {
// 當前節點的左節點是為要刪除的節點
if (this.left != null && this.left.no == no) {
this.left = null
return
}
// 當前節點的右節點是為要刪除的節點
if (this.right != null && this.right.no == no) {
this.right = null
return
}
// 說明當前節點的左/右節點不是要刪除的節點,則向左遞歸刪除和向右遞歸刪除
if (this.left != null) {
this.left.delNode(no)
}
if (this.right != null) {
this.right.delNode(no)
}
}
}
思考
如果要刪除的節點是非葉子節點,現在我們不希望將該非葉子節點為根節點的子樹刪除,需要指定規則, 假如規定如下:
刪除A節點
(1)如果該非葉子節點A只有一個子節點B,則子節點B替代節點A
(2)如果該非葉子節點A有左子節點B和右子節點C,則讓左子節點B替代節點A。
19.12 順序存儲的二叉樹
19.12.1 順序存儲二叉樹的概念
基本說明
從數據存儲來看,數組存儲方式和樹的存儲方式可以相互轉換,即數組可以轉換成樹,樹也可以轉換成數組,看下面的示意圖。
19.12.2 順序存儲二叉樹的遍歷
需求:給你一個數組 Array(1,2,3,4,5,6,7),要求以二叉樹前序遍歷的方式進行遍歷。
前序遍歷的結果應當為 1,2,4,5,3,6,7
代碼實現:
package com.atguigu.chapter19.binarytree
object ArrayTreeDemo01 {
def main(args: Array[String]): Unit = {
val arr = Array(1, 2, 3, 4, 5, 6, 7)
val arrayTree = new ArrayTree(arr)
println("-----前序遍歷(數組)的結果-----")
arrayTree.preOrder()
println("-----中序遍歷(數組)的結果-----")
arrayTree.infixOrder()
println("-----后序遍歷(數組)的結果-----")
arrayTree.postOrder()
}
}
// 把數組當成二叉樹,把數組以二叉樹前序遍歷的方式進行遍歷
class ArrayTree(val arr: Array[Int]) {
// 為了方便,對 preOrder 進行一個方法重載
def preOrder():Unit = {
this.preOrder(0)
}
// 為了方便,對 infixOrder 進行一個方法重載
def infixOrder():Unit = {
this.infixOrder(0)
}
// 為了方便,對 postOrder 進行一個方法重載
def postOrder():Unit = {
this.postOrder(0)
}
// 前序遍歷二叉樹,即前序遍歷數組
// 前序遍歷:先輸出父節點,再遍歷左子樹和右子樹。
def preOrder(index: Int): Unit = {
if (arr == null || arr.length == 0) {
println("數組為空,不能按照二叉樹遍歷的方式進行遍歷")
}
// 先輸出當前節點信息
println(arr(index)) // index 初始化值為0,即對應 root 節點
// 向左遞歸輸出左子樹
if ((index * 2 + 1) < arr.length) {
preOrder(index * 2 + 1)
}
// 向右遞歸輸出右子樹
if ((index * 2 + 2) < arr.length) {
preOrder(index * 2 + 2)
}
}
// 中序遍歷二叉樹,即中序遍歷數組
// 中序遍歷::先遍歷左子樹,再輸出父節點,再遍歷右子樹。
def infixOrder(index: Int): Unit = {
if (arr == null || arr.length == 0) {
println("數組為空,不能按照二叉樹遍歷的方式進行遍歷")
}
// 向左遞歸輸出左子樹
if ((index * 2 + 1) < arr.length) {
infixOrder(index * 2 + 1)
}
// 輸出當前節點信息
println(arr(index)) // index 初始化值為0,即對應 root 節點
// 向右遞歸輸出右子樹
if ((index * 2 + 2) < arr.length) {
infixOrder(index * 2 + 2)
}
}
// 后序遍歷二叉樹,即后序遍歷數組
// 后序遍歷::先遍歷左子樹,再遍歷右子樹,最后輸出父節點。
def postOrder(index: Int): Unit = {
if (arr == null || arr.length == 0) {
println("數組為空,不能按照二叉樹遍歷的方式進行遍歷")
}
// 向左遞歸輸出左子樹
if ((index * 2 + 1) < arr.length) {
postOrder(index * 2 + 1)
}
// 向右遞歸輸出右子樹
if ((index * 2 + 2) < arr.length) {
postOrder(index * 2 + 2)
}
// 輸出當前節點信息
println(arr(index)) // index 初始化值為0,即對應 root 節點
}
}
輸出結果如下:
-----前序遍歷(數組)的結果-----
1
2
4
5
3
6
7
-----中序遍歷(數組)的結果-----
4
2
5
1
6
3
7
-----后序遍歷(數組)的結果-----
4
5
2
6
7
3
1
19.13 二叉排序樹
19.13.1 先看一個需求
需求
給你一個數組 (7, 3, 10, 12, 5, 1, 9),要求能夠高效的完成對數組的查詢和添加。
解決方案分析
1、使用數組
數組未排序,優點:直接在數組尾添加,速度快。缺點:查找速度慢。
數組排序,優點:可以使用二分查找,查找速度快。缺點:為了保證數組有序,在添加新數據時,找到插入位置后,后面的數據需整體移動,速度慢。
2、使用鏈式存儲-鏈表
不管鏈表是否有序,查找速度都慢,添加數據速度比數組快,不需要數據整體移動。
3、使用二叉排序樹
19.13.2 二叉排序樹的介紹
二叉排序樹:BST: (Binary Sort(Search) Tree),對於二叉排序樹的任何一個非葉子節點,要求左子節點的值比當前節點的值小,右子節點的值比當前節點的值大。
特別說明:如果有相同的值,可以將該節點放在左子節點或右子節點,比如針對前面的數組 (7, 3, 10, 12, 5, 1, 9),插入2,則對應的二叉排序樹為:
19.13.3 二叉排序樹的創建和遍歷
一個數組創建成對應的二叉排序樹,並使用中序遍歷二叉排序樹,比如 數組為 Array(7, 3, 10, 12, 5, 1, 9)。
示例代碼如下:
package com.atguigu.chapter19.binarytree
object BinarySortTreeDemo01 {
def main(args: Array[String]): Unit = {
val arr = Array(7, 3, 10, 12, 5, 1, 9)
// 測試
// 創建一顆二叉排序樹
val binarySortTree = new BinarySortTree
// 添加節點
for (item <- arr) {
binarySortTree.add(new Node(item))
}
// 遍歷二叉排序樹
binarySortTree.infixOrder()
}
}
// 定義二叉排序樹
class BinarySortTree {
var root: Node = null
// 添加節點
def add(node: Node): Unit = {
if (root == null) {
root = node
}else {
root.add(node)
}
}
// 中序遍歷
def infixOrder(): Unit = {
if (root != null) {
root.infixOrder()
} else {
println("當前二叉樹為空,不能遍歷")
}
}
}
// 定義某某節點
class Node(val value: Int) {
var left: Node = null
var right: Node = null
// 添加節點
def add(node: Node): Unit = {
if (node == null) { // 如果節點為空,則直接返回
return
}
// 如果要插入的節點的值小於當前節點的值
if (node.value < this.value) {
if (this.left == null) { // 說明當前節點沒有左子節點
this.left = node
} else {
// 遞歸地進行添加
this.left.add(node)
}
} else { // 如果要插入的節點的值大於或等於當前節點的值
if (this.right == null) { // 說明當前節點沒有有子節點
this.right = node
} else {
// 遞歸地進行添加
this.right.add(node)
}
}
}
// 中序遍歷:先遍歷左子樹,再輸出父節點,再遍歷右子樹。
def infixOrder(): Unit = {
// 向左遞歸輸出左子樹
if (this.left != null) {
this.left.infixOrder()
}
// 輸出當前節點信息
printf("節點信息 no=%d \n", value)
// 向右遞歸輸出右子樹
if (this.right != null) {
this.right.infixOrder()
}
}
}
輸出結果如下:
節點信息 no=1
節點信息 no=3
節點信息 no=5
節點信息 no=7
節點信息 no=9
節點信息 no=10
節點信息 no=12
19.13.4 二叉排序樹的刪除
二叉排序樹的刪除情況比較復雜,有下面三種情況需要考慮
1) 刪除葉子節點 (比如:2, 5, 9, 12),即該節點下沒有左右子節點
2) 刪除只有一顆子樹的節點 (比如:1),即該節點有左子節點或者右子節點
3) 刪除有兩顆子樹的節點. (比如:7, 3, 10),該節點有左子節點和右子節點
思路分析
完整代碼如下:
package com.atguigu.chapter19.binarytree
object BinarySortTreeDemo01 {
def main(args: Array[String]): Unit = {
val arr = Array(7, 3, 10, 12, 5, 1, 9, 2)
// 測試
// 創建一顆二叉排序樹
val binarySortTree = new BinarySortTree
// 添加節點
for (item <- arr) {
binarySortTree.add(new Node(item))
}
// 中序遍歷二叉排序樹
binarySortTree.infixOrder()
println("----------刪除節點----------")
// 測試刪除葉子節點
// binarySortTree.delNode(2)
// binarySortTree.delNode(5)
// binarySortTree.delNode(9)
// binarySortTree.delNode(12)
// 中序遍歷二叉排序樹
// binarySortTree.infixOrder()
// 測試刪除只有一顆子樹的節點
// binarySortTree.delNode(1)
// 中序遍歷二叉排序樹
// binarySortTree.infixOrder()
// 測試刪除有兩顆子樹的節點
binarySortTree.delNode(7)
// 中序遍歷二叉排序樹
binarySortTree.infixOrder()
}
}
// 定義二叉排序樹
class BinarySortTree {
var root: Node = null
// 添加節點
def add(node: Node): Unit = {
if (root == null) {
root = node
} else {
root.add(node)
}
}
// 中序遍歷
def infixOrder(): Unit = {
if (root != null) {
root.infixOrder()
} else {
println("當前二叉樹為空,不能遍歷")
}
}
// 根據值,查找某個節點
def search(value: Int): Node = {
if (root != null) {
return root.search(value)
} else {
return null
}
}
// 根據值,查找某個節點的父節點
def searchParent(value: Int): Node = {
if (root != null) {
return root.searchParent(value)
} else {
return null
}
}
// 刪除葉子節點 (比如:2, 5, 9, 12),即該節點下沒有左右子節點
// 刪除只有一顆子樹的節點 (比如:1),即該節點有左子節點或者右子節點
// 刪除有兩顆子樹的節點. (比如:7, 3, 10),該節點有左子節點和右子節點
def delNode(value: Int): Unit = {
if (root == null) {
return
}
// 根據值,查找某個節點
val tagetNode = search(value)
if (tagetNode == null) {
return
}
// 程序能執行到這里說明找到要刪除的節點了
// 根據值,查找某個節點的父節點
val parentNode = searchParent(value)
// 要刪除的節點 tagetNode 是不是葉子節點
if (tagetNode.left == null && tagetNode.right == null) { // tagetNode是 葉子節點
if (parentNode.left != null && parentNode.left.value == value) { // 該葉子節點 tagetNode 是父節點 parentNode 的左子節點
parentNode.left = null
} else { // 該葉子節點 tagetNode 是父節點 parentNode 的右子節點
parentNode.right = null
}
} else if (tagetNode.left != null && tagetNode.right != null) { // 要刪除的節點 tagetNode 有兩顆子樹
// 找到刪除節點的右子樹的最小值,刪除並返回最小值
val value = delRightTreeMin(tagetNode)
// 將要刪除的節點的值替換為最小值
tagetNode.value = value
} else { // 要刪除的節點 tagetNode 只有一顆子樹
if (tagetNode.left != null) { // 要刪除的節點的左子節點不為空,右子節點為空 <= 注意
// 判斷 tagetNode 是 parentNode 的左子節點還是右子節點
if (parentNode.left.value == value) { // 左子節點
parentNode.left = tagetNode.left
} else { // 右子節點
parentNode.right = tagetNode.left
}
} else { // 要刪除的節點的左子節點為空,右子節點不為空 <= 注意
// 判斷 tagetNode 是 parentNode 的左子節點還是右子節點
if (parentNode.left.value == value) { // 左子節點
parentNode.left = tagetNode.right
} else { // 右子節點
parentNode.right = tagetNode.right
}
}
}
}
// 要刪除的節點的右子樹的最小值的節點,並返回最小值
def delRightTreeMin(node: Node): Int = {
var tagetRight = node
// 循環找到要刪除的節點的右子樹的最小值
while (tagetRight.left != null) {
tagetRight = tagetRight.left
}
val minValue = tagetRight.value
delNode(minValue)
return minValue
}
}
// 定義某某節點
class Node(var value: Int) {
var left: Node = null
var right: Node = null
// 添加節點
def add(node: Node): Unit = {
if (node == null) { // 如果節點為空,則直接返回
return
}
// 如果要插入的節點的值小於當前節點的值
if (node.value < this.value) {
if (this.left == null) { // 說明當前節點沒有左子節點
this.left = node
} else {
// 遞歸地進行添加
this.left.add(node)
}
} else { // 如果要插入的節點的值大於或等於當前節點的值
if (this.right == null) { // 說明當前節點沒有有子節點
this.right = node
} else {
// 遞歸地進行添加
this.right.add(node)
}
}
}
// 中序遍歷:先遍歷左子樹,再輸出父節點,再遍歷右子樹。
def infixOrder(): Unit = {
// 向左遞歸輸出左子樹
if (this.left != null) {
this.left.infixOrder()
}
// 輸出當前節點信息
printf("節點信息 no=%d \n", value)
// 向右遞歸輸出右子樹
if (this.right != null) {
this.right.infixOrder()
}
}
// 根據值,查找某個節點
def search(value: Int): Node = {
// 先判斷當前節點是否是要刪除的節點
if (value == this.value) {
return this
} else if (value < this.value) { // 不是,向左遞歸查找
if (this.left == null) {
return null
} else {
return this.left.search(value)
}
} else { // 不是,向右遞歸查找
if (this.right == null) {
return null
} else {
return this.right.search(value)
}
}
}
// 根據值,查找某個節點的父節點
def searchParent(value: Int): Node = {
// 先判斷當前節點的左子節點的值或右子節點的值是否是要查找的值
if ((this.left != null && this.left.value == value) || (this.right != null && this.right.value == value)) { // 是
return this
} else { // 不是,向左遞歸查找或者向右遞歸查找
if (this.left != null && value < this.value) { // 先判斷向左的條件
return this.left.searchParent(value)
} else if (this.right != null && value > this.value) { // 先判斷向右的條件
return this.right.searchParent(value)
} else {
return null
}
}
}
}
輸出結果如下:
節點信息 no=1
節點信息 no=2
節點信息 no=3
節點信息 no=5
節點信息 no=7
節點信息 no=9
節點信息 no=10
節點信息 no=12
----------刪除節點----------
節點信息 no=2
節點信息 no=3
節點信息 no=5
節點信息 no=1
節點信息 no=9
節點信息 no=10
節點信息 no=12
19.14 其它二叉樹
1、線索二叉樹:利用沒有用到的節點反向指向其父節點。
2、赫夫曼二叉樹(哈夫曼樹/最優二叉樹) [數據編碼、解碼 和 數據壓縮、解壓]
3、平衡二叉樹(平衡二叉搜索樹/AVL樹) 常用的實現方法有:紅黑樹、替罪羊樹、伸展樹等
4、B樹
5、B+樹
6、2-3樹
7、圖
