鏈表介紹
鏈表是有序的列表,但是它在內存中是存儲如下: 鏈表在內存中不一定是連續分佈. [鏈表是學習 樹,森林,圖的基礎] // 鏈表的優點 1. 插入,刪除,很快, 缺點是檢索速度慢. => 樹結構(AVL, 紅黑樹)
單鏈表的介紹
單鏈表(帶頭結點) 邏輯結構示意圖如下
單鏈表的應用實例
使用帶head頭的單向鏈表實現 –水滸英雄排行榜管理
1)完成對英雄人物的增刪改查操作, 注: 刪除和修改,查找
2)第一種方法在添加英雄時,直接添加到鏈表的尾部
3)第二種方式在添加英雄時,根據排名將英雄插入到指定位置
(如果有這個排名,則添加失敗,並給出提示)
代碼
package datastructure
/**
* @author cherry
* @create 2019-10-01-18:50
*/
import util.control.Breaks._
object SingleLinkedListDemo {
def main(args: Array[String]): Unit = {
//測試一把鏈表的創建和遍歷
val singleLinkedList = new SingleLinkedList()
//創建三個英雄
val node1 = new HeroNode(1, "宋江", "及時雨")
val node2 = new HeroNode(2, "盧俊義", "玉麒麟")
val node3 = new HeroNode(3, "吳用", "智多星")
val node4 = new HeroNode(4, "林沖", "豹子頭")
singleLinkedList.add(node4)
singleLinkedList.add(node1)
singleLinkedList.add(node3)
singleLinkedList.add(node2)
/*singleLinkedList.addByOrder(node4)
singleLinkedList.addByOrder(node1)
singleLinkedList.addByOrder(node3)
singleLinkedList.addByOrder(node2)*/
//singleLinkedList.addByOrder(node2)
println("鏈表的情況")
singleLinkedList.list()
//測試對結點的修改
val node5 = new HeroNode(4, "公孫勝", "入雲龍")
singleLinkedList.update(node5)
printf("鏈表修改後的情況\n")
singleLinkedList.list()
/* //測試一把刪除結點
println()
println()
println("刪除後的鏈表情況")
singleLinkedList.del(3)
singleLinkedList.del(1)
singleLinkedList.del(4)
singleLinkedList.del(12)
singleLinkedList.list()*/
}
}
//創建單鏈表的類
class SingleLinkedList {
//創建頭結點,指向該鏈表的頭部
val head = new HeroNode(-1, "","")
//刪除一個結點
def del(no:Int): Unit = {
//判斷是否爲空
if(isEmpty()) {
println("鏈表爲空")
return
}
//讓temp指向head
var temp = head
var flag = false
//遍歷,讓temp指向,要刪除的結點的前一個結點
breakable {
while (true) {
if (temp.next.no == no) {
//找到
flag = true
break()
}
//判斷temp是否指向鏈表的倒數第二個結點
if (temp.next.next == null) {
break()
}
//讓temp後移,實現遍歷
temp = temp.next
}
}
//判斷flag 的情況
if(flag) {//找到
temp.next = temp.next.next
} else {
printf("你要刪除的結點%d 不存在", no)
}
}
//修改結點信息
def update(heroNode: HeroNode): Unit = {
//如果鏈表爲空
if(isEmpty()) {
println("鏈表爲空")
return
}
//輔助指針,幫助我們定位到修改的結點
var temp = head.next
//需要定義一個變量,標識是否找到該節點
var flag = false
breakable {
while (true) {
//比較
if (temp.no == heroNode.no) {
flag = true
break()
}
//判斷temp 是不是到最後
if (temp.next == null) { //最後
break()
}
//讓temp後移,實現遍歷
temp = temp.next
}
}
//判斷
if(flag) { //找到
temp.nickname = heroNode.nickname
temp.name = heroNode.name
} else { // 沒有找到
printf("你要修改的%d英雄不存在.", heroNode.no)
}
}
//添加英雄到單鏈表
//默認直接將人加入到鏈表的最後
def add(heroNode: HeroNode): Unit = {
//思路
//1. 先找到鏈表的最後結點
//2. 然後讓最後結點.next = 新的結點
//因爲head 不動,因此我們使用一個輔助指針來完成定位
var temp = head
breakable {
while (true) {
if (temp.next == null) {
//temp已經到鏈表的最後
break()
}
temp = temp.next // 讓temp後移
}
}
//當退出while時,temp指向最後
temp.next = heroNode
}
//按照no編號的從小到大進行插入
def addByOrder(heroNode: HeroNode): Unit = {
//讓temp 指向 head
var temp = head
var flag = false // 標識是否已經存在這個編號的結點
breakable {
//將temp 定位要添加的結點前一個位置,即將新的結點,添加到temp 後面
while (true) {
//判斷是否是最後一個,如果就是最後一個,也找到位置
if (temp.next == null) {
break()
}
if (temp.next.no == heroNode.no) { //說明no已經存在
flag = true
break()
} else if (temp.next.no > heroNode.no) { //說明heroNode 就應該加入到temp的後面
break()
}
//將temp 後移,實現遍歷
temp = temp.next
}
}
//當退出while循環後,
if(flag) { //說明已經有no
printf("已經存在no=%d 人物", heroNode.no)
} else {
heroNode.next = temp.next //
temp.next = heroNode
}
}
//遍歷單向鏈表
//思路
//1. 仍然讓temp幫助進行遍歷
// 2.判斷鏈表是否空,空退出, 不爲空,就遍歷,直到最後結點
def list(): Unit = {
if(isEmpty()) {
println("鏈表爲空,無法遍歷")
return
}
//記住,head不能動, 爲什麼指向 head.next,因爲我們有效的數據不包括head
var temp = head.next
breakable {
while (true) {
//輸出當前這個結點的信息
printf("no=%d name=%s nickname=%s --> \n", temp.no, temp.name, temp.nickname)
//是否是最後結點
if (temp.next == null) { //是最後結點
break()
}
//如果不是最後的結點,讓temp 後移
temp = temp.next
}
}
}
def isEmpty(): Boolean = {
head.next == null
}
}
class HeroNode(hNo: Int, hName: String, hNickname: String) {
val no = hNo
var name = hName
var nickname = hNickname
var next: HeroNode = null //默認爲null
}
測試添加
測試修改
測試刪除
測試按no排序添加
雙向鏈表的應用實例
使用帶head頭的雙向鏈表實現 –水滸英雄排行榜
管理單向鏈表的缺點分析:
1)單向鏈表,查找的方向只能是一個方向,而雙向鏈
表可以向前或者向後查找。
2)單向鏈表不能自我刪除,需要靠輔助節點 ,而雙向
鏈表,則可以自我刪除,所以前面我們單鏈表刪除
時節點,總是找到temp的下一個節點來刪除的(認真
體會).
3)示意圖幫助理解刪除
代碼
package datastructure
/**
* @author cherry
* @create 2019-10-01-20:57
*/
import scala.util.control.Breaks.{break, breakable}
object DoubleLinkedListDemo {
def main(args: Array[String]): Unit = {
//測試一下雙向遍歷的添加和遍歷
//測試一把鏈表的創建和遍歷
val doubleLinkedList = new DoubleLinkedList()
//創建三個英雄
val node1 = new HeroNode2(1, "宋江", "及時雨")
val node2 = new HeroNode2(2, "盧俊義", "玉麒麟")
val node3 = new HeroNode2(3, "吳用", "智多星")
val node4 = new HeroNode2(4, "林沖", "豹子頭")
doubleLinkedList.add(node1)
doubleLinkedList.add(node2)
doubleLinkedList.add(node3)
doubleLinkedList.add(node4)
println("雙向鏈表的情況")
doubleLinkedList.list()
val node5 = new HeroNode2(4, "公孫勝", "入雲龍")
doubleLinkedList.update(node5)
println("雙向鏈表的修改後情況")
doubleLinkedList.list()
//刪除的測試
doubleLinkedList.del(1)
doubleLinkedList.del(2)
doubleLinkedList.del(3)
doubleLinkedList.del(4)
println("雙向鏈表的刪除後情況")
doubleLinkedList.list()
}
}
//添加,遍歷,修改,刪除
class DoubleLinkedList {
//創建頭結點,指向該鏈表的頭部
val head = new HeroNode2(-1, "","")
//刪除結點,因爲雙向鏈表可以實現自我刪除,因此我們讓temp指向要刪除的結點即可
def del(no:Int): Unit = {
if(isEmpty()) {
println("鏈表空")
return
}
var temp = head.next
var flag = false
breakable {
while (true) {
if (temp.no == no) {
flag = true
break()
}
if (temp.next == null) {
break()
}
temp = temp.next //遍歷
}
}
//當退出while循環後,如果flag=true,則temp 就指向要刪除的結點
if(flag) {
//刪除
temp.pre.next = temp.next
if(temp.next != null) {
temp.next.pre = temp.pre
}
}else{
printf("要刪除的no=%d,不存在", no)
}
}
//添加英雄到單鏈表
//默認直接將人加入到鏈表的最後
def add(heroNode: HeroNode2): Unit = {
//思路
//1. 先找到鏈表的最後結點
//2. 然後讓最後結點.next = 新的結點
//因爲head 不動,因此我們使用一個輔助指針來完成定位
var temp = head
breakable {
while (true) {
if (temp.next == null) {
//temp已經到鏈表的最後
break()
}
temp = temp.next // 讓temp後移
}
}
//當退出while時,temp指向最後
temp.next = heroNode
heroNode.pre = temp//必須
}
//遍歷單向鏈表
//思路
//1. 仍然讓temp幫助進行遍歷
// 2.判斷鏈表是否空,空退出, 不爲空,就遍歷,直到最後結點
def list(): Unit = {
if(isEmpty()) {
println("鏈表爲空,無法遍歷")
return
}
//記住,head不能動, 爲什麼指向 head.next,因爲我們有效的數據不包括head
var temp = head.next
breakable {
while (true) {
//輸出當前這個結點的信息
printf("no=%d name=%s nickname=%s --> ", temp.no, temp.name, temp.nickname)
//是否是最後結點
if (temp.next == null) { //是最後結點
break()
}
//如果不是最後的結點,讓temp 後移
temp = temp.next
}
}
}
def isEmpty(): Boolean = {
head.next == null
}
//修改結點信息
def update(heroNode: HeroNode2): Unit = {
//如果鏈表爲空
if(isEmpty()) {
println("鏈表爲空")
return
}
//輔助指針,幫助我們定位到修改的結點
var temp = head.next
//需要定義一個變量,標識是否找到該節點
var flag = false
breakable {
while (true) {
//比較
if (temp.no == heroNode.no) {
flag = true
break()
}
//判斷temp 是不是到最後
if (temp.next == null) { //最後
break()
}
//讓temp後移,實現遍歷
temp = temp.next
}
}
//判斷
if(flag) { //找到
temp.nickname = heroNode.nickname
temp.name = heroNode.name
} else { // 沒有找到
printf("你要修改的%d英雄不存在.", heroNode.no)
}
}
}
class HeroNode2(hNo: Int, hName: String, hNickname: String) {
val no = hNo
var name = hName
var nickname = hNickname
var next: HeroNode2 = null //指向後一個結點 默認爲null
var pre: HeroNode2 = null // 指向前一個結點,默認null
}
測試刪除
單鏈表的常見面試題有如下:
- 求單鏈表中有效節點的個數
- 查找單鏈表中的倒數第k個結點 【新浪面試題】
- 單鏈表的反轉【騰訊面試題,有點難度】
- 從尾到頭打印單鏈表 【百度,要求方式1:反向遍歷 。 方式2:Stack棧】
- 合併兩個有序的單鏈表,合併之後的鏈表依然有序
單向環形鏈表的應用場景
丟手帕問題
環形單向鏈表介紹
環形單向鏈表的應用實例
ØJosephu 問題
Josephu 問題爲:設編號爲1,2,… n的n個人圍坐一圈,約定編號爲k(1<=k<=n)的人從1開始報數,數到m 的那個人出列,它的下一位又從1開始報數,數到m的那個人又出列,依次類推,直到所有人出列爲止,由此產生一個出隊編號的序列。
Ø提示
用一個不帶頭結點的循環鏈表來處理Josephu 問題:先構成一個有n個結點的單循環鏈表,然後由k結點起從1開始計數,計到m時,對應結點從鏈表中刪除,然後再從被刪除結點的下一個結點又從1開始計數,直到最後一個結點從鏈表中刪除算法結束。
Ø思路分析示意圖
- 約瑟夫問題解決的思路
代碼實現
package datastructure
/**
* @author cherry
* @create 2019-10-01-21:21
*/
import util.control.Breaks._
object JosephDemo {
def main(args: Array[String]): Unit = {
//測試一把
val josephu = new Joseph()
josephu.addBoy(5)
josephu.list()
//測試一下游戲
//josephu.countBoy(2,2,5) // 3 - 5 - 2 - 1 - 4
josephu.countBoy2(2,2,5) //3 - 5 - 2 - 1 - 4
}
}
class Joseph {
//定義一個first,初始爲null
var first: Boy = null
//分析:因爲在出圈過程中,first 實際上是沒有什麼用處,因此可以去掉
def countBoy2(startNO:Int, countNum:Int, boyNums:Int): Unit = {
//做下數據的驗證
if(first == null || startNO > boyNums || startNO <= 0) {
println("輸出參數有誤,不能玩遊戲")
return
}
//1創建輔助指針
var helper = first
//2讓 helper 移動到 first 的上一個
breakable {
while (true) {
if (helper.next == first) {
break()
}
helper = helper.next
}
}
//3. 讓helper 移動 k - 1
for(i <- 0 until startNO - 1) {
helper = helper.next
}
breakable {
while (true) {
//4. 讓first 和 helper 再移動 m - 1 個位置
for (i <- 0 until countNum - 1) { //for循環結束後,helper.next就指向了要刪除小孩
helper = helper.next
}
//5. helper.next就指向了要刪除的小孩
printf("小孩no=%d出圈\n", helper.next.no)
helper.next = helper.next.next
//判斷是否已經只有一個小孩了
if (helper == helper.next) {
break()
}
}
}
printf("最後留在圈中的小孩是no=%d\n",helper.no)
}
//小孩出圈
def countBoy(startNO:Int, countNum:Int, boyNums:Int): Unit = {
//做下數據的驗證
if(first == null || startNO > boyNums || startNO <= 0) {
println("輸出參數有誤,不能玩遊戲")
return
}
//1創建輔助指針
var helper = first
//2讓 helper 移動到 first 的上一個
breakable {
while (true) {
if (helper.next == first) {
break()
}
helper = helper.next
}
}
//3.讓 first 移動 k - 1 個位置, 同時讓helper 也做相應的移動
for(i <- 0 until startNO - 1) {
first = first.next
helper = helper.next
}
breakable {
while (true) {
//4. 讓first 和 helper 再移動 m - 1 個位置
for (i <- 0 until countNum - 1) { //for循環結束後,first就指向了要刪除小孩
first = first.next
helper = helper.next
}
//5. first 就指向了要刪除的小孩
printf("小孩no=%d出圈\n", first.no)
first = first.next
helper.next = first
//判斷是否已經只有一個小孩了
if (first == helper) {
break()
}
}
}
printf("最後留在圈中的小孩是no=%d\n",first.no)
}
//添加小孩,形成環形鏈表
def addBoy(boyNums: Int): Unit = {
var curBoy: Boy = null
for (i <- 1 to boyNums) {
//創建小孩對象
val boy = new Boy(i)
//處理第一個小孩, 就形成環形
if (i == 1) {
first = boy
curBoy = boy
first.next = first
} else {
curBoy.next = boy
boy.next = first
curBoy = boy
}
}
}
//遍歷環形鏈表
def list(): Unit = {
//判斷鏈表是否爲空
if(first == null) {
println("沒有小孩")
return
}
var curBoy = first
breakable {
while (true) {
printf("no=%d \n", curBoy.no)
if (curBoy.next == first) {
//表示curBoy已經是最後這個
break()
}
//curBoy後移
curBoy = curBoy.next
}
}
}
}
//小孩類
class Boy(bNo: Int) {
val no = bNo
var next: Boy = null
}
測試代碼