java 二维链表_Java数据结构与算法----数组与链表
數據類型
1 數據類型介紹
數據類型的分類(按照結構劃分):線性結構和非線性結構
線性結構:線性結構作為最常用的數據結構,其特點是數據元素之間存在一對一的線性關系
線性結構有兩種不同的存儲結構,即順序存儲結構(數組)和 鏈式存儲結構(鏈表),順序存儲的線性表為順序表,順序表中存儲的元素是連續的
鏈式存儲結構的線性表稱為鏈表,鏈表中的存儲的元素不一定是連續的,元素節點中存放數據元素以及相鄰元素的地址信息
吸納行結構常見的有:數組,隊列,鏈表,棧
非線性結構:
非線性結構包括:二維數組,多維數組,廣義表,樹結構,圖結構
2 數據類型之——稀疏數組
2.1 引入實例
分析問題:因為該二維數組的很多值都是默認值0,因此記錄了很多沒有意義的數據,所以要用到稀疏數組來節省空間
2.2 稀疏數組的基本介紹
當一個數組中大部分元素為0,或者為同一個值的數組時,可以使用稀疏數組來保存該數組
稀疏數組的處理方式是:記錄數組一共有幾行幾列,有多少個不同的值
把具有不同值的元素的行列和值記錄在一個小規模的數組中,從而縮小程序的規模
3
2.3 應用實例
使用稀疏數組來保存類似前面的二維數組(棋盤,地圖等)
把稀疏數組存盤,并且可以重新恢復原來的二維數組
2.4 思路分析
二維數組轉稀疏數組的思路
1) 遍歷原始的二維數組,得到有效數據的個數 sum
2) 根據sum就可以創建稀疏數組 sparsArr int[sum + 1][3]
3) 將二維數組的有效數據存入到稀疏數組
稀疏數組轉原始的二維數組的思路
1) 先讀取稀疏數組的第一行,根據第一行的數據,創建原始的二維數組,比如上面的chessArr = int[11][11]
2) 再讀取稀疏數組后面幾行的內容,并賦給原始的二位數組即可
public class SparseArray {
public static void main(String[] args) {
//創建一個棋盤(原始的二維數組11*11)
//0:表示沒有棋子,1:表示黑色棋子,2:表示藍色棋子
int[][] chessArr = new int[11][11];
chessArr[1][2] = 1;
chessArr[2][3] = 2;
chessArr[4][5] = 2;
//輸出原始的二維數組
System.out.println('原始的二維數組');
for (int[] row : chessArr) {//對二維數組中的整行遍歷
for (int data : row) {
System.out.printf('%d\t', data);
}
System.out.println();
}
//將二維數組轉稀疏數組的思想
//1.先遍歷二維數組,得到非0數據的個數
int sum = 0;
for (int[] row : chessArr) {
for (int data : row) {
if (data != 0) {
sum++;
}
}
}
System.out.println('sum=' + sum);
//創建對應的稀疏數組
int[][] sparsArr = new int[sum + 1][3];
//給稀疏數組賦值
sparsArr[0][0] = chessArr.length;
sparsArr[0][1] = chessArr[0].length;
sparsArr[0][2] = sum;
//遍歷二維數組,將非0的值存放到稀疏數組中
int count = 1;//計數器,用于記錄是第幾個非零數據
for(int i = 0; i
for (int j =0; j
if (chessArr[i][j]!=0){
sparsArr[count][0] = i;
sparsArr[count][1] = j;
sparsArr[count][2] = chessArr[i][j];
count++;
}
}
}
//輸出稀疏數組
System.out.println('稀疏數組');
for (int[]row:sparsArr){
for (int data:row){
System.out.printf('%d\t',data);
}
System.out.println();
}
System.out.println('還原的二維數組');
//將稀疏數組恢復成原始的二維數組
//1.根據sparsArr第一行創建二維數組
int[][] chessArr2 = new int[sparsArr[0][0]][sparsArr[0][1]];
//2.再讀取稀疏數組的后幾行的數據(從第二行開始),并賦值給新的二維數組即可
for (int i = 1; i<=sparsArr[0][2];i++){
chessArr2[sparsArr[i][0]][sparsArr[i][1]] = sparsArr[i][2];
}
for (int[] row:chessArr2){
for (int data:row){
System.out.printf('%d\t',data);
}
System.out.println();
}
}}
3 單向隊列
3.1 隊列介紹隊列是一個有序列表,可以用數組
或是鏈表
來實現
遵循先入先出
的原則,即:先存入隊列的數據要先取出,后存入的要后取出
示意圖(用數組模擬隊列)
3.2 數組模擬隊列隊列本身是有序列表,若使用數組的結構來存儲隊列的數據,則隊列數組的聲明如上圖所示,其中maxSize是該隊列的最大容量
因為隊列的輸出、輸入是分別從前后端來處理的,因此需要兩個變量front及rear分別記錄隊列前后端的下標,front會隨著數據輸出而改變,rear會隨著數據的插入而改變
3.3 思路分析
我們將數據存入隊列的時候稱為“addQueue”,addQueue的處理需要有兩個步驟將尾指針(rear)往后移: rear + 1 (隊列為空時:rear == front)
若為指針rear小于隊列的最大下標,maxSize-1, 則將數據存入rear所指的數組元素中,否則無法存入數據。(隊列滿時:rear == maxSize-1)
3.4 代碼實現import?java.util.Scanner;public?class?ArrayQueueDemo?{
public?static?void?main(String[]?args)?{
ArrayQueue?arrayQueue?=?new?ArrayQueue(5);
Scanner?s?=?new?Scanner(System.in);
boolean?loop?=?true;
while?(loop){
System.out.println('a(add):添加數據');
System.out.println('g(get):得到數據');
System.out.println('s(show):顯示數據');
System.out.println('q(quit):退出程序');
char?text?=?s.next().charAt(0);
switch?(text){
case?'a':
System.out.println('請輸入要添加的數據');
arrayQueue.addQueue(s.nextInt());
break;
case?'g':
try{
System.out.println('得到的數據為');
arrayQueue.getQueue();
}catch?(Exception?e){
System.out.println(e.getMessage());
}
break;
case?'s':
arrayQueue.show();
break;
case?'q':
s.close();//關閉輸入器防止異常
loop?=?false;
break;
}
}
}}//創建數組隊列類class?ArrayQueue{
private?int?maxSize;//表示數組的最大容量
private?int?front;//頭指針
private?int?rear;//尾指針
private?int[]?arr;//該數組用于存放數據,模擬隊列
//創建數組隊列的構造器
public?ArrayQueue(int?maxSize){
this.maxSize?=?maxSize;
this.arr?=?new?int[maxSize];
front?=?-1;//指向隊列頭部,分析出front是指向隊列頭的前一個位置
rear?=?-1;//指向隊列尾部,指向隊列尾(即隊列最后一個數據)
}
//判斷隊列是否為空
public?boolean?isEmpty(){
return?front?==?rear;
}
//判斷隊列是否滿了
public?boolean?isFull(){
return?rear?==?maxSize?-?1;
}
//向隊列中添加數據
public?void?addQueue(int?n){
//如果滿了則輸出異常不添加數據
if?(isFull()){
System.out.println('錯誤:隊列已滿不能再添加');
return;
}
//沒滿,則指針后移,向指針所指的格子中添加數據
rear++;
arr[rear]?=?n;
}
//從隊列中獲得數據
public?int?getQueue(){
//如果隊列是空的,拋出異常
if?(isEmpty()){
throw?new?RuntimeException('隊列是空的,不能取出數據');
}
//指針后移返回數據
front++;
return?arr[front];
}
//將隊列格式化輸出
public?void?show(){
if?(isEmpty()){
System.out.println('隊列是空的,沒有數據');
return;
}
for?(int?i?=?0;?i
System.out.printf('arr[%d]?=?%d\n',i,arr[i]);
}
}}
3.5 存在的問題目前數組使用一次就不能繼續用了,沒有達到復用的效果,造成了空間浪費
將這個數組使用算法,改進成一個環形隊列(用取模的方式%)
(類比鐘表:maxSize為12)
4 數組模擬隊列的改進——環形隊列
4.1 思路分析對front變量的含義做一個調整:front就指向隊列的第一個元素,也就是說arr[front] 就是隊列的第一個元素front的初始值為0
對rear變量的含義做一個調整:rear指向隊列的最后一個元素的后一個位置,因為希望空出一個空間(rear指向的空間
)做約定(該約定用來判斷隊列是不是滿了)rear 的初始值是0
當隊列滿時,條件為 (rear + 1)%maxSize == front【滿了】
(該條算法是在判斷rear節點的下一個節點是不是front,如果是,則這個環形隊列滿了)
隊列為空的條件是:rear == front【空的】
當我們這樣分析,隊列中的有效的數據個數(rear+maxSize-front)%maxSize
(類似于鐘表,5點和16點在表盤上差了幾點:(5+12-16)%12 = 1)環形隊列示意圖
4.2 注意約定的位置(內容永遠為空的位置,最后一個數據位的下一個位置) 是在動態變化的,永遠位于最后一個元素的后一個位置
(注意:環形隊列是放不滿的(在當前算法下),一定會有一個空余的位置)
addQueue時和單向隊列的區別
1)單向隊列:先后移,再插入數據(rear指向的位置永遠是有數據的)
2)環形隊列:先插入數據,再后移(rear指向的位置永遠是空的)
getQueue時和單向隊列的區別:
1)單向隊列:front++,返回arr[front](front的初始值是-1)
2)環形隊列:返回arr[front],front++(front的初始值是0)
show()和單向隊列的區別
1)單向隊列:從i=0,遍歷到i=arr.length()(將數據全部打印出)
2)環形隊列:從i=front,遍歷到 i=front+size,(左閉右開)
(其中size = (rear + maxSize -front)%maxSize)
isFull和單向隊列的區別:
1)單向隊列:rear == masSize-1
2)環形隊列:(rear+1)%maxSize ==front
(rear的下一個元素是front,則說明該隊列滿了)環形隊列滿的條件
環形隊列空的條件
4.3 代碼實現
import java.util.Scanner;public class CircleArrayQueueDemo {
public static void main(String[] args) {
CircleArrayQueue circleArrayQueue = new CircleArrayQueue(5);//說明:設置4,但是有效數據最大是3,有一個空間是作為約定的
Scanner s = new Scanner(System.in);
boolean loop = true;
while (loop) {
System.out.println('a(add):添加數據');
System.out.println('g(get):得到數據');
System.out.println('s(show):顯示數據');
System.out.println('q(quit):退出程序');
System.out.println('h(head):查看隊列的第一個數據');
char text = s.next().charAt(0);
switch (text) {
case 'a':
System.out.println('請輸入要添加的數據');
circleArrayQueue.addQueue(s.nextInt());
break;
case 'g':
try {
System.out.println('得到的數據為' + circleArrayQueue.getQueue());
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 's':
circleArrayQueue.show();
break;
case 'q':
s.close();//關閉輸入器防止異常
loop = false;
break;
case 'h':
circleArrayQueue.headQueue();
break;
}
}
}}//創建數組隊列類class CircleArrayQueue {
private int maxSize;//表示數組的最大容量
private int front;//頭指針
private int rear;//尾指針
private int[] arr;//該數組用于存放數據,模擬隊列
//創建數組隊列的構造器
public CircleArrayQueue(int maxSize) {
this.maxSize = maxSize;
this.arr = new int[maxSize];
front = 0;//指向隊列頭部,即隊列的第一個元素
rear = 0;//指向隊列尾部,指向隊列尾的后一個數據
}
//判斷隊列是否為空
public boolean isEmpty() {
return front == rear;
}
//判斷隊列是否滿了
public boolean isFull() {
//如果尾指針的下一個元素是front,則滿了
return (rear + 1) % maxSize == front;
}
//向隊列中添加數據
public void addQueue(int n) {
//如果滿了則輸出異常不添加數據
if (isFull()) {
System.out.println('錯誤:隊列已滿不能再添加');
return;
}
//沒滿,向當前位置所指的格子中添加數據(因為rear所指的格子是空的)
arr[rear] = n;
//指針后移
rear = (rear + 1) % maxSize;//防止出界
}
//從隊列中獲得數據
public int getQueue() {
//如果隊列是空的,拋出異常
if (isEmpty()) {
throw new RuntimeException('隊列是空的,不能取出數據');
}
//這里需要分析出front是指向隊列的第一個元素
//先輸出,再后移front
//1. 先把front對應的值保留到一個臨時變量
//2. 將front后移
//3. 將臨時變量返回
int temp = arr[front];
front = (front + 1) % maxSize;
return temp;
}
//將隊列格式化輸出
public void show() {
if (isEmpty()) {
System.out.println('隊列是空的,沒有數據');
return;
}
//思路:從front開始遍歷,遍歷多少個元素
for (int i = front; i < front + (rear - front + maxSize) % maxSize; i++) {
System.out.printf('arr[%d] = %d\n', i % maxSize, arr[i % maxSize]);
}
}
//顯示隊列的頭數據,不是取出數據
public int headQueue() {
if (isEmpty()) {
throw new RuntimeException('隊列是空的,沒有數據');
}
return arr[front];
}}
5 單鏈表
5.1 單鏈表介紹
鏈表是有序的列表,但是它在內存中的存儲如下
(實際在內存中的存儲)
邏輯結構示意圖如下
總結鏈表是以結點的方式來存儲的,鏈式存儲
每個節點包含data域,next域:指向下一個節點
如圖:發現在內存中鏈表的各個節點不一定是有序存儲的
鏈表分帶頭節點和不帶頭結點的鏈表,根據實際需求來確定
5.2 鏈表的創建按順序直接在尾部添加
每個節點中的內容
添加(創建)的過程
1)先創建一個head頭節點,作用是表示單鏈表的頭(標明此鏈表的頭部位置)
2)后面我們每添加一個節點,就直接加入到鏈表的最后
3)遍歷:通過一個輔助節點遍歷,幫助遍歷整個鏈表
按照編號順序添加
根據排名將英雄插入到指定的位置,(如果該位置已經存在英雄,則添加失敗)
思路:
1)首先找到新添加的節點位置,是通過輔助變量(指針)找到的,通過遍歷得到
2)新的節點.next = temp.next
3)temp.next = 新的節點
5.3 節點的修改
通過輔助(指針)遍歷鏈表,發現節點內部編號相等,則與新的節點內容進行互換
5.4 節點的刪除
思路:我們先找到需要刪除的節點的前一個結點
temp.next = temp.next.next
(不用考慮被刪除的節點是最后一個的情況,temp.next = null和temp.next.next 在這種情況下是一樣的)
被刪除的節點,將不會有其他的引用所指向,會被GC回收
5.5 代碼實現public?class?SingleLinkedListDemo?{
public?static?void?main(String[]?args)?{
//測試
//先創建節點
HeroNode?hero1?=?new?HeroNode(1,?'宋江',?'及時雨');
HeroNode?hero2?=?new?HeroNode(2,?'盧俊義',?'玉麒麟');
HeroNode?hero3?=?new?HeroNode(3,?'吳用',?'智多星');
HeroNode?hero4?=?new?HeroNode(4,?'林沖',?'豹子頭');
//創建鏈表
SingleLinkedList?singleLinkedList?=?new?SingleLinkedList();
singleLinkedList.addByOrder(hero1);
singleLinkedList.addByOrder(hero2);
singleLinkedList.addByOrder(hero2);
singleLinkedList.add(hero3);
singleLinkedList.add(hero4);
singleLinkedList.List();
singleLinkedList.delete(hero4);
singleLinkedList.delete(hero3);
singleLinkedList.delete(hero2);
singleLinkedList.delete(hero1);
System.out.println('刪除后');
singleLinkedList.List();
}}class?SingleLinkedList?{
//先初始化一個頭結點,頭節點不要動,不要存放具體的數值
HeroNode?head?=?new?HeroNode(0,?'',?'');
//添加節點到單向鏈表
//思路:當不考慮編號順序時
//1.?找到當前鏈表的最后節點
//2.?將最后這個節點的next指向新的節點
public?void?add(HeroNode?heroNode)?{
//因為head節點不能動,因此我們需要一個輔助遍歷temp
HeroNode?temp?=?head;
//遍歷鏈表找到最后一個節點
while?(true)?{
//找到最后一個節點,終止循環
if?(temp.next?==?null)?{
break;
}
//如果沒有找到,將temp向后移動
temp?=?temp.next;
}
//while退出時,temp已經指向的最后的節點
temp.next?=?heroNode;
}
public?void?addByOrder(HeroNode?heroNode)?{
//因為頭節點不能動,因此我們仍然通過一個輔助指針(變量)來幫助找到添加的位置
//因為單鏈表,因此我們找到的temp是位于添加位置的前一個結點,否則插入不進去
HeroNode?temp?=?head;//輔助指針,初始值在head
boolean?flag?=?false;//標識:表示該插入的對象是否已經在鏈表中存在了,默認為false(沒有存在)
//遍歷,從head開始到鏈表尾
while?(true)?{
//如果已經在鏈表尾
if?(temp.next?==?null)?{
break;
}
//如果找到位置,就在temp后面插入
if?(temp.next.heroNo?>?heroNode.heroNo)?{//temp的heroNo不大于,但是temp.next大于,說明插入位置在temp和temp.next之間
break;//找到位置,退出循環
}
if?(temp.next.heroNo?==?heroNode.heroNo)?{
flag?=?true;//說明該節點存在
break;
}
//如果以上條件都不滿足,將temp后移
temp?=?temp.next;
}
//此時得到了一個flag值或者一個temp位置
//首先判斷flag
if?(flag)?{
//說明該節點已經存在
System.out.printf('節點%d已經存在,不能再添加\n',?heroNode.heroNo);
}?else?{
//該節點不存在的話,就在temp后面插入新的節點
heroNode.next?=?temp.next;
temp.next?=?heroNode;
}
}
//修改節點
public?void?update(HeroNode?newHeroNode)?{
//首先確定一下鏈表是否為空
if?(head.next?==?null)?{
System.out.println('鏈表為空,無法修改');
}
//創建輔助結點來遍歷鏈表
HeroNode?temp?=?head.next;
//創建flag變量,判斷該節點是否找到
boolean?flag?=?false;
while?(true)?{
//如果已經遍歷到尾節點,終止
if?(temp?==?null)?{
break;
}
if?(temp.heroNo?==?newHeroNode.heroNo)?{
//找到該節點
flag?=?true;
break;
}?else?{
temp?=?temp.next;
}
}
//循環結束后通過flag值判斷是否修改
if?(flag)?{
temp.heroName?=?newHeroNode.heroName;
temp.nickName?=?newHeroNode.nickName;
//next和no都不用變
}?else?{
System.out.printf('沒有編號為%d的節點,無法修改\n',?newHeroNode.heroNo);
}
}
//刪除節點
//思路:
//1.head不能動,因此我們需要一個temp輔助節點找到待刪除節點的前一個節點
//2.說明我們在比較時,是temp.next.heroNo?和要刪除的節點的heroNo比較
public?void?delete(HeroNode?delHeroNode)?{
//如果鏈表為空,無法刪除
if?(head.next?==?null)?{
System.out.println('鏈表為空,無法刪除\n');
return;
}
//構建輔助節點,幫忙遍歷鏈表
HeroNode?temp?=?head;
//flag表示是否找到該節點的前一位,默認為false
boolean?flag?=?false;
while?(true)?{
//如果遍歷到最后一位,說明該節點不存在,終止循環
if?(temp.next?==?null)?{
break;
}
if?(temp.next.heroNo?==?delHeroNode.heroNo)?{
//找到了要刪除的節點的上一位
flag?=?true;
break;
}?else?{
//沒有找到,繼續往后走
temp?=?temp.next;
}
}
//循環終止后通過判斷flag值決定是否刪除節點
if?(flag)?{
//如果要刪除的節點在最后一位,則將上一位的next指向null
/*if?(temp.next.next==null){
temp.next?=?null;
}
else?{
//要刪除的節點不在最后一位
temp.next?=?temp.next.next;
}*/
//如果要刪除的節點在最后一位,則temp.next.next本身就等于null,以上兩種情況可以合并
temp.next?=?temp.next.next;
}?else?{
System.out.printf('沒有編號為%d的節點,無法刪除\n',?delHeroNode.heroNo);
}
}
public?void?List()?{
//判斷鏈表是否為空
if?(head.next?==?null)?{
System.out.println('鏈表為空');
return;
}
//因為頭節點不能動,所以需要一個輔助節點來遍歷
HeroNode?temp?=?head.next;
while?(true)?{
//判斷是否到鏈表最后
if?(temp?==?null)?{
break;
}
//如果還沒有遍歷到最后
System.out.println(temp);
//將temp后移
temp?=?temp.next;
}
}}//定義HeroNode,每一個HeroNode對象就是一個節點class?HeroNode?{
public?int?heroNo;
public?String?heroName;
public?String?nickName;
HeroNode?next;
public?HeroNode(int?heroNo,?String?heroName,?String?nickName)?{
this.heroNo?=?heroNo;
this.heroName?=?heroName;
this.nickName?=?nickName;
}
//為了顯示方法,我們重寫toString
public?String?toString()?{
return?'HeroNode?[no?=?'?+?heroNo?+?',?name?=?'?+?heroName?+?',?nickname?=?'?+?nickName?+?']';
}}
5.6 單鏈表面試題
5.6.1 求單鏈表中有效節點的個數
韓老師的方法寫在了測試類中,為靜態方法:
/**
*
* @param head 鏈表的頭節點
* @return 返回鏈表的長度
*/
public static int getLength(HeroNode head){
//空鏈表
if (head.next == null){
return 0;
}
int result = 0;
//輔助接點遍歷,將頭節點排除在外
HeroNode cur = head.next;
while (cur!=null){
result++;
cur = cur.next;
}
return result;
}//同時因為head為SingleLinkedList類中的private屬性,所以在SingleLinkedList中添加一個getter方法
public HeroNode getHead() {
return head;
}
//==========================
我寫在了SingleLinkedList類中,為實例方法,不需要參數,可以直接使用對象調用//方法:獲取單鏈表的節點的個數(如果是帶頭節點的,需要不統計頭節點)
public?int?getLength2(){
int?result?=?0;
HeroNode?temp?=?head;
while?(true){
if?(temp.next?==?null){
break;
}
temp?=?temp.next;
result++;
}
return?result;
}
我感覺在實際情況中,韓老師的方法更對一些,因為SingleLinkedList在實際應用情況下是封裝好的,外部是不能夠更改內部的,所以應該用外部的方法取實現要求。
5.6.2 查找單鏈表中的倒數第k個節點
思路:編寫一個方法,接受head節點,同時接受一個index
index表示是倒數第index個節點
先把鏈表從頭到尾遍歷,得到鏈表的總長度getLength
得到size之后,我們從鏈表的第一個節點(非head)開始遍歷(size-index)個,就可以得到
如果找到了,則返回該節點,否則返回null示意圖
代碼示例:
/**
*
* @param head 鏈表的頭節點
* @param k
* @return 返回倒數第K個節點
*/
public static HeroNode findLastKNode(HeroNode head, int k){
//如果鏈表為空,返回null
if (head.next == null){
return null;
}
//找到鏈表的長度
int length = getLength(head);
//先做一個index校驗
if ( k <= 0 || k > length){
return null;
}
//輔助節點幫助遍歷
HeroNode cur = head.next;
//遍歷K次
for (int i = 0; i < length - k; i++) {
cur = cur.next;
}
return cur;
}
5.6.3 從尾到頭打印單鏈表(反向遍歷)
思路:方式1:先將單鏈表進行反轉操作,然后再遍歷即可,但是這樣做的問題會破壞原來的單鏈表的結構,不建議
方式2:利用棧數據結構,將各個節點壓入到棧中,利用棧的先進后出的特點實現了逆序打印的效果
總結
以上是生活随笔為你收集整理的java 二维链表_Java数据结构与算法----数组与链表的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux java main 参数设置
- 下一篇: ios rsa java_一篇搞定RSA