雙向鏈表
單向鏈表:
- 單鏈表是一種鏈式存取的數據結構,用一組地址任意(不需要地址連續)的存儲單元存放線性表中的數據元素,可以使用零碎的空間
- 鏈表中的數據是以節點來表示的,每個節點的構成:元素(數據元素的映象)+ 指針(指示後繼元素存儲位置),元素就是存儲數據的存儲單元value,指針就是連接每個節點的地址數據next
- 單鏈表是單向的,當訪問一個節點後,只能接着訪問後繼節點
- 單鏈表可以設置頭節點,頭節點爲空
單向鏈表的特點是隻能遵循鏈式的順序從鏈表頭節點到尾節點
而雙向鏈表是在單向鏈表的基礎上,新增一個前驅指針pre,可以指向前驅節點、後繼節點
Java實現雙向鏈表
雙向鏈表是在單向鏈表的基礎上實現的:
- 節點對象HeroNode新增一個前驅指針pre
- 更新、遍歷與單鏈表方法相同
- 因爲有兩個指針pre、next,插入、刪除時,需要注意修改前驅結點、後繼節點的指針
package com.company.doubleLinkedList;
import org.junit.Test;
/**
* @author zfk
* 雙向鏈表
*/
public class DoubleLinkedListDemo {
@Test
public void test(){
HeroNode hero1 = new HeroNode(1, "宋江", "及時雨");
HeroNode hero2 = new HeroNode(2, "盧俊義", "玉麒麟");
HeroNode hero3 = new HeroNode(3, "吳用", "智多星");
HeroNode hero4 = new HeroNode(4, "林沖", "豹子頭");
HeroNode hero5 = new HeroNode(5, "花榮", "小李廣");
DoubleLinkedList doubleLinkedList = new DoubleLinkedList();
// doubleLinkedList.add(hero1);
// doubleLinkedList.add(hero2);
// doubleLinkedList.add(hero3);
// doubleLinkedList.add(hero4);
// doubleLinkedList.add(hero5);
doubleLinkedList.addByOrder(hero3);
doubleLinkedList.addByOrder(hero1);
doubleLinkedList.addByOrder(hero2);
doubleLinkedList.addByOrder(hero5);
doubleLinkedList.addByOrder(hero4);
doubleLinkedList.list();
System.out.println("=== 修改後 ===");
doubleLinkedList.update(new HeroNode(4,"公孫勝","入雲龍"));
doubleLinkedList.list();
System.out.println("=== 刪除3號 ===");
doubleLinkedList.delete(3);
doubleLinkedList.list();
}
}
/**
*
* 每個HeroNode對象代表一個鏈表節點
*
*/
class HeroNode{
private int no;
public String name;
public String nickName;
//指向下一節點
private HeroNode next;
//指向上一節點
private HeroNode pre;
public HeroNode(int no, String name, String nickName) {
this.no = no;
this.name = name;
this.nickName = nickName;
}
public int getNo() {
return no;
}
public HeroNode getNext() {
return next;
}
public void setNext(HeroNode next) {
this.next = next;
}
public HeroNode getPre() {
return pre;
}
public HeroNode setPre(HeroNode pre) {
this.pre = pre;
return this;
}
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickName='" + nickName + '\'' +
'}';
}
}
/**
*
* 定義DoubleLinkedList雙鏈表
*
*/
class DoubleLinkedList{
//初始化一個頭節點
private HeroNode head = new HeroNode(0,"","");
public HeroNode getHead() {
return head;
}
//添加節點到雙鏈表
// 當不考慮編號順序
public void add(HeroNode node){
//需要一個輔助節點 遍歷 鏈表
HeroNode temp = head;
//遍歷鏈表,找到最後
while (true){
//最後的結點 next == null
if (temp.getNext() == null){
break;
}
temp = temp.getNext();
}
//當退出while循環時,temp指向鏈表最後節點,node指向前節點,形成雙向鏈表
temp.setNext(node);
node.setPre(temp);
}
// 有序插入
public void addByOrder(HeroNode heroNode){
//通過輔助指針temp找到 插入位置, temp位於添加位置前一個節點
HeroNode temp = head;
//標識添加的編號是否存在
boolean flag = false;
while (true){
//如果temp在鏈表最後
if (temp.getNext() == null){
break;
}
//位置找到了,在temp的後面插入
if (temp.getNext().getNo() > heroNode.getNo()){
break;
}
else if (temp.getNext().getNo() == heroNode.getNo()){
//編號存在,插入失敗
flag = true;
break;
}
//後移temp
temp = temp.getNext();
}
//判斷flag
if (flag){
System.out.println("=== 準備插入的Hero編號"+heroNode.getNo()+"已存在 ===");
}
else {
//插入到鏈表中,需要修改temp(插入節點前一節點)、temp.next(插入節點後一節點)
heroNode.setNext(temp.getNext());
heroNode.setPre(temp);
if (temp.getNext() != null){
temp.getNext().setPre(heroNode);
}
temp.setNext(heroNode);
}
}
//通過一個輔助變量 遍歷 鏈表
public void list(){
//判斷鏈表是否爲空
if (head.getNext() == null){
System.out.println("=== 鏈表爲空 ===");
return;
}
//需要一個輔助變量遍歷
HeroNode temp = head.getNext();
while (true){
//輸出節點信息
System.out.println(temp);
//判斷是否到鏈表最後
if (temp.getNext() == null){
break;
}
//將temp後移
temp = temp.getNext();
}
}
//根據no修改節點信息
public void update(HeroNode heroNode){
//判斷是否爲空
if (head.getNext() == null){
System.out.println("=== 鏈表爲空 ===");
return;
}
//找到要修改的節點
HeroNode temp = head.getNext();
//表示是否找到該節點
boolean flag = false;
while (true){
//已經遍歷完鏈表
if (temp == null){
break;
}
//找到該節點
if (temp.getNo() == heroNode.getNo()){
flag = true;
break;
}
temp = temp.getNext();
}
//根據 flag 判斷要修改的節點
if (flag){
//因爲temp獲得的是對象的引用,可以直接修改
temp.name = heroNode.name;
temp.nickName = heroNode.nickName;
}else {
//沒有找到
System.out.println("沒有找到編號 = "+heroNode.getNo()+"不能修改");
}
}
/**
* 刪除節點
* 1. 先找到要刪除的節點 temp
* 2. temp.pre.next = temp.nxet; temp.next.pre = temp.pre
* 3. 被刪除的節點,不會其他引用指向,會被JVM垃圾回收
* */
public void delete(int no){
//判斷當前鏈表是否爲空
if (head.getNext() == null){
System.out.println("=== 鏈表爲空 ===");
return;
}
//輔助節點
HeroNode temp = head.getNext();
//是否找到待刪除節點
boolean flag = false;
while (true){
//已經到鏈表的最後
if (temp == null) {
break;
}
//如果找到帶刪除節點的前一個節點temp
if (temp.getNo() == no){
flag = true;
break;
}
//temp後移
temp = temp.getNext();
}
//判斷flag
if (flag){
//找到,轉移temp前後節點的指針
temp.getPre().setNext(temp.getNext());
//如果是最後一個節點就不需要執行下面命令,否則會出現空指針異常
if (temp.getNext() != null){
temp.getNext().setPre(temp.getPre());
}
}
else {
System.out.println("=== 沒有找到該節點"+no+" ===");
}
}
}
測試結果:
關於單鏈表的相關問題:
- 求單鏈表中有效節點的個數
- 查找倒數第k個數據
- 反轉鏈表
- 從尾到頭打印單鏈表
- 合併兩個有序鏈表,合併後鏈表也有序
雙向鏈表的解決方法和單鏈表類似,因爲存在前驅指針,可以更簡單的解決
單向環形鏈表
單向環形鏈表是在單向鏈表的基礎上,把鏈表尾的next指針指向了第一個節點,除去頭節點
注意:因爲不存在頭指針,需要我們自己設置一個first指針,表示第一個節點
關於單向環形鏈表,有一個經典的問題:約瑟夫問題
Josephu 問題爲:設編號爲1,2,… n的n個人圍坐一圈,約定編號爲k(1<=k<=n)的人從1開始報數,數到m 的那個人出列,它的下一位又從1開始報數,數到m的那個人又出列,依次類推,直到所有人出列爲止,由此產生一個出隊編號的序列。
Java單向環形鏈表實現約瑟夫問題
設置一個first指針替代頭指針
- 添加節點:因爲約瑟夫問題沒必要設置有序添加,這裏是無序添加方法
需要注意:第一個節點next指向自己;當添加一個節點時需要修改兩個指針(前一個節點的next、當前節點的next指向first) - 遍歷:設置一個輔助指針curBoy在first位置,從first開始移動,curBoy.next == first表示結束
- 出列:設置一個輔助指針helper指向鏈表尾,開始報數時同時移動first、helper指針,移動m位,first指針指向的就是要出圈的節點,修改指針
helper.next = first.next
,first指針後移
package com.company.josepfu;
import org.junit.Test;
/**
* @author zfk
*
* 環形鏈表完成約瑟夫問題
*/
public class MyTest{
@Test
public void test(){
CircleSingleLinkedList list = new CircleSingleLinkedList();
list.addBoy(7);
list.show();
list.out(2,4,7);
}
}
/**
* 環形單向鏈表
*/
class CircleSingleLinkedList{
//創建一個first節點,當前沒有編號
private Boy first = new Boy(-1);
/**
* @param nums 小孩數目
*
* 添加小孩節點,構件成環形鏈表
*/
public void addBoy(int nums){
//nums數據校驗
if (nums < 1){
System.out.println("nums 的值不正確");
return;
}
//輔助指針
Boy curBoy = null;
//創建環形鏈表
for (int i = 1;i <= nums;i++){
//根據編號創建小孩節點
Boy boy = new Boy(i);
//如果是第一個小孩
if (i == 1){
first = boy;
//構成環
first.setNext(first);
//讓curBoy輔助指針指向第一個小孩
curBoy = first;
}
else {
//當新增一個boy,需要修改兩個指針
curBoy.setNext(boy);
boy.setNext(first);
//curBoy指針後移
curBoy = boy;
}
}
}
/**
* 遍歷當前的環形鏈表
*/
public void show(){
//判斷是否爲空
if (first == null){
System.out.println("=== 環形鏈表爲空 ===");
return;
}
//first不能動,需要一個輔助指針完成遍歷
Boy curBoy = first;
while (true){
System.out.println("小孩的編號:"+ curBoy.getNo());
//判斷遍歷完畢
if (curBoy.getNext() == first){
break;
}
//curBoy後移
curBoy = curBoy.getNext();
}
}
/**
* @param startNo 從第幾個小孩開始數
* @param countNum 表示數幾下
* @param nums 表示最初有多少個小孩在圈中
*
* 小孩出列
*/
public void out(int startNo,int countNum,int nums){
//數據校驗
if (first == null || startNo <1 || startNo > nums){
System.out.println("=== 參數輸入異常 ===");
return;
}
//輔助指針
Boy helper = first;
//helper指針指向環形鏈表最後的節點
while (true){
if (helper.getNext() == first){
break;
}
helper = helper.getNext();
}
//先移動到 startNo 處
for (int j = 0;j < startNo - 1;j++){
first = first.getNext();
helper = helper.getNext();
}
//當報數時,讓first和helper指針同時移動countNum -1 ,然後出圈
while (true){
//說明只有一個節點
if (helper == first){
break;
}
//讓first、helper移動countNum -1
for (int j = 0;j < countNum - 1;j++){
first = first.getNext();
helper = helper.getNext();
}
//這時first指向的節點就是要出圈的節點
System.out.println("小孩:"+first.getNo()+" 出圈");
//節點出圈
first = first.getNext();
helper.setNext(first);
}
System.out.println("最後在圈中的小孩:"+first.getNo());
}
}
/**
* 環形鏈表節點
*/
class Boy {
private int no;
private Boy next;
public Boy(int no) {
this.no = no;
}
public int getNo() {
return no;
}
public Boy setNo(int no) {
this.no = no;
return this;
}
public Boy getNext() {
return next;
}
public Boy setNext(Boy next) {
this.next = next;
return this;
}
}
測試: