鏈表是一種數據結構,和數組同級。鏈表中的數據是以結點來表示的,每個結點構成:元素+指針 。元素就是存儲數據單元,指針就是連接每個結點的地址數據。比如,java中我們經常使用的ArrayList,其實現原理是數組,而LinkedList的實現原理就是鏈表。鏈表在進行循環遍歷時效率不高,但是在插入和刪除是優勢明顯,下面對單鏈表做介紹
一、簡介
以”結點的序列”表示線性表稱爲”線性鏈表(單鏈表)”。也就是說它實際上是由結點(Node)組成的,一個鏈表擁有不定數量的結點其數據在內存中存儲是不連續的,它存儲的數據分散在內存中,每個結點只能也只有它能知道下一個結點的存儲位置。由N各節點(Node)組成單向鏈表,每一個Node記錄本Node的數據及下一個Node的存儲位置。向外暴露的只有一個頭節點(Head),我們對鏈表的所有操作,都是直接或者間接地通過其頭節點來進行的。 如圖所示:
上圖中最左側的結點就是頭結點,但是新添加的結點的順序是從左到右的,添加的新結點會被作爲新節點。最先添加的結點對下一個結點的引用可以爲空。
二、鏈表的結點變量
上圖中藍色的部分就是結點的結構。
對象(data):存放結點值的數據域。
引用(next):存放結點的直接後繼的地址(位置)的指針域(鏈域)。
三、鏈表的具體存儲方式
1)用一組任意的存儲單元來存放線性表的結點(這組存儲單元既可以是連續的,也可以是不連續的),如下圖所示。
2)鏈表中的結點邏輯次序和物理次序不一定相同。爲了正確表示結點見的邏輯關係,在存儲每個結點值的同事,還必須存儲指示其後繼結點的地址或位置信息(成爲指針或鏈)。
下圖簡單的描述了單向鏈表存儲情況。存儲是分散的,每一個結點只要記錄下一個結點就把所有數據串了起來,形成了一個單向的鏈表。
四、單鏈表的建立
單鏈表的建立有頭插法、尾插法兩種方法:
1、頭插法:在頭結點(頭結點的數據域可以存放數據標題、表長等信息,也可以不存儲任何信息,其指針域存儲第一個結點的首地址)之後插入數據,其特點就是:讀入的數據順序與線性表的邏輯正好相反,如圖所示。
注: 頭插法建單鏈表是將鏈表右端看成固定的,鏈表不斷向左延伸而得到的。頭插法最先得到的是尾結點(鏈表的最右邊)
從上圖可以看出頭插法就是從一個空表開始,重複讀入數據,形成新結點,將讀入數據存放到新結點的數據域中,然後將新結點插入到當前鏈表的表頭結點之後。
算法實現如下:
Step1.定義一個鏈表的結點
package test;
/**
* Created by DELL on 2017/11/24.
* 鏈表結點的實現
*/
public class Node<T> {
/**
* 下一個結點
*/
Node<T> next;
/**
* 結點存儲的數據
*/
T val;
public Node(T val, Node<T> next) {
this.next = next;
this.val = val;
}
}
Step2.定義一個單鏈表頭插法的操作類
package test;
/**
* Created by DELL on 2017/11/24.
* 單鏈表的頭插法操作類
*/
public class LinearList<T> {
Node<T> head ; //鏈表的頭結點
Node<T> tail ; //鏈表的尾結點
static int length;//定義鏈表長度
public LinearList() {
//空鏈表
this.head = null;
this.tail = null;
}
//頭插法 鏈表 指針指向this.head
void add(T val){
this.head = new Node<T>(val,this.head);
if(this.tail == null){
//此時將尾結點固定,不斷改變的是頭結點
this.tail = this.head;
}
//鏈表長度加1
length ++ ;
}
public static void main(String[] args){
LinearList<Integer> list = new LinearList<Integer>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
Node<Integer> head = list.head;
while(head!=null){
System.out.println(head.val);
head = head.next;
}
System.out.println("鏈表的長度爲:"+length);
}
/**
* 讀入的數據順序與線性表的邏輯正好相反
* 運行結果
* 5
* 4
* 3
* 2
* 1
* 鏈表的長度爲:5
*
*/
}
2.尾插法:尾插法就是將鏈表的左端固定,鏈表不斷向右延伸也就是說將每次插入的新結點放在鏈表的尾部,如圖所示:
注:尾插法建立鏈表時,頭指針固定不動, 故必須設立一個搜索指針,向鏈表右邊延伸,則整個算法應設立三個鏈表指針,即頭指針、搜索指針、申請單元指針。尾插法最先得到的是頭結點
從上圖可以看出,尾插法就是從一個空表開始,重複讀入數據,生成新結點,將讀入數據存放在新結點的數據域中,然後將新結點插入到當前鏈表的表尾上,直到讀入結束標誌爲止。
算法實現
Step1.定義結點類
package test;
/**
* Created by DELL on 2017/11/24.
* 鏈表結點的實現
*/
public class Node<T> {
/**
* 下一個結點
*/
Node<T> next;
/**
* 結點存儲的數據
*/
T val;
public Node(T val, Node<T> next) {
this.next = next;
this.val = val;
}
}
Step2.定義一個單鏈表的尾插法的操作類
package test;
/**
* Created by DELL on 2017/11/25.
*/
public class TailLinerList<T> {
Node<T> head ; //鏈表的頭結點
Node<T> tail; //鏈表的尾結點
static int length;
public TailLinerList() {
//空鏈表
this.tail = null;
this.head = null;
}
public void tialInsert(T val){
//判斷是否有尾結點,如果沒有尾結點則說明此時是一個空的鏈表
if(this.tail == null){
//定義一個結點
this.tail = new Node<T>(val,null);
//由於是插入第一個元素此時頭結點和尾結點是同一個
//此時將頭結點固定,不斷該改變的是尾結點
this.head = this.tail;
}else{
//從插入第二個元素開始進入這部分
//此時尾結點的next應該指向新結點
this.tail.next = new Node<T>(val,null);
//把鏈表的尾結點變成新插入進來的結點
this.tail = this.tail.next;
}
//鏈表長度加1
length++;
}
public static void main(String[] args){
TailLinerList<Integer> tailLinerList = new TailLinerList<Integer>();
tailLinerList.tialInsert(6);
tailLinerList.tialInsert(7);
tailLinerList.tialInsert(8);
tailLinerList.tialInsert(9);
tailLinerList.tialInsert(10);
Node<Integer> headList = tailLinerList.head;
while(headList !=null ){
System.out.println(headList.val);
headList = headList.next;
}
System.out.println("鏈表的長度爲:"+length);
}
/**
* 讀入的數據順序與線性表的邏輯相同
* 運行結果
* 6
* 7
* 8
* 9
* 10
* 鏈表的長度爲:5
*/
}
綜上所述:因爲單鏈表的特殊結構,即只能從頭向尾遍歷,所以向頭插時所用的語句會比向尾插少幾個,向尾插時還要多一個指針指向尾結點,而用頭插法時就不用,但用頭插法時最先輸入的數據會排在鏈表的最後,輸出時即變成了輸入時的逆序輸出,看起來不如用尾插法那樣和輸入的順序一樣的形勢更舒服些 。
五、鏈表中動態存儲分配常用的標準函數
1)malloc(size)
在內存動態存儲區申請一個長度爲size字節的連續空間,但是需設立一申請單元指針,但malloc()函數得到的指針並不是指向結構體(是由一系列具有相同類型或不同類型的數據構成的數據集合)的指針,需使用強制類型轉換,將其轉換成結構體型指針。
2)calloc(n,size)
在內存的動態存儲區申請n個長度爲size字節的連續空間,函數返回值爲分配空間的首地址。若此函數未被成功執行,函數返回值爲0。
3)free(p)
釋放由指針p所指向的存儲單元,而存儲單元的大小是最近一次調用malloc()或calloc()函數時所申請的存儲空間。