線性表的學習
學習目標
- 線性表的定義
- 線性表的儲存方式和表達方式
- 基本實現
- 基本操作實現
- 雙向連結串列插入和刪除實現
- 迴圈單連結串列和迴圈雙向連結串列的結構特點
1. 線性表:
- 定義:零個或多個資料元素所構成的有限序列
- 儲存方式:順序儲存結構和鏈式儲存結構
- 抽象資料型別描述
public interface IList<E>{
void clear();//線性表清空操作
boolean isEmpty();// 判空
int size(); //長度
E get(int i);//通過下標獲取元素
void insert(int i,E t);// 插入元素到特定位置
void remove(int i);// 移除元素
int indexOf(E t);// 查詢元素
void display();//列印元素
}
複製程式碼
2. 線性表順序儲存結構:
- 定義: 用順序儲存方法儲存的線性表簡稱為順序表(Sequential List)。
- 節點儲存地址的計算:
- 假設每個節點佔用c個儲存單元
- 其中第一個單元的儲存地址則是該結點的儲存地址; 並設表中開始結點a1的儲存地址(簡稱為基地址)是LOC(a1)
- 所以結點ai的儲存地址LOC(ai): LOC(ai)= LOC(a1)+(i-1)*c 1≤i≤n
- 順序表的特點
- 邏輯上相鄰的結點其物理位置亦相鄰。
- 儲存密度高; 儲存密度= 資料元素所需的儲存空間/該資料元素實際所佔空間;需要預先分配"足夠應用"的空間,可能會造成儲存空間浪費
- 便於隨機儲存
- 不便於隨機插入刪除
3. 順序表基本實現和分析
- 刪除操作實現及分析
- 程式碼實現
public T remove(int i) {
// 首先,先判度下標 i 是否合法
if (i < 0 || i > this.lenght)
throw new IndexOutOfBoundsException("Index: " + i + ", Size: " + this.lenght);
//獲取刪除的元素
T removeObj = (T) objects[i];
//把下標為i及其後的元素,往前移移一位
for (int j = i; j < this.lenght - 1; j++) {
objects[j] = objects[j + 1];
}
//把最後一個元素置空,幫助垃圾回收
objects[lenght - 1] = null;
//當前線性表長度減一
--lenght;
return removeObj;
}
複製程式碼
- 時間複雜度分析
- 在n個元素的順序表中,刪除第i各元素,則0<=i<=n-1
- 假設刪除的概率相同,則p = 1/n
- 刪除第i個元素後,需要移動 n-i-1個元素
- 平均移動的次數為 (1/n) * (n-i-1)求和
- 所以時間複雜度為:O(n)
- 插入操作及分析
- 程式碼實現
public void insert(int i, T t) {
// 首先,先判度下標 i 是否合法
if (i < 0 || i > this.lenght)
throw new IndexOutOfBoundsException("Index: " + i + ", Size: " + this.lenght);
//判斷是否超出順序表的容量
if (this.lenght >= this.objects.length)
throw new ArrayIndexOutOfBoundsException("length = " + this.lenght + " Capacity" + this.initCapacity);
//把下標為i及其後的元素,往後移移一位
for (int j = this.lenght - 1; j >= i; --j) {
objects[j + 1] = objects[j];
}
//插入元素
objects[i] = t;
lenght++;
}
複製程式碼
- 時間複雜度分析
- 在長度為 n 的順序表中,在第i個位置插入一個元素, 0<= i <= n
- 插入元素前需要將第i個元素開始往後移動,需要移動n-i個元素
- 假設插入每個位置的概率相同,則p = 1/(n+1)
- 所以,平均移動次數為 (1/(n+1)) * (n-i) 求和 (0<= i <= n)
- 則,平均時間複雜度為: O(n)
- 查詢元素操作及其分析
- 程式碼實現
public int indexOf(E e) {
//先判斷 元素是否為空,可防止空指標的出現
if (t == null) {
//為空則,返回順序表中空元素的下標
for (int i = 0; i < this.lenght; i++)
if (objects[i] == null)
return i;
} else {
//不為空,則返回與之匹配的元素下標
for (int i = 0; i < this.lenght; i++)
if (t.equals(objects[i]))
return i;
}
return -1;
}
複製程式碼
- 時間複復雜度分析
- 假設在n個元素的順序表中,第i個元素為查詢的元素x
- 那麼,比較的次數為i+1次,如果沒有找到,則需要比較n次
- 假設查詢每個元素的概率相同,p= (1/n)
- 所以,平均比較次數為 (1/n)*(i+1)求和 (0<=i<=n-1)
- 時間複雜度為:O(n)
4. 連結串列的概念
- 定義:
- 連結方式儲存的線性表簡稱為連結串列(Linked List)。
- 儲存結構定義:
- 用一組任意的儲存單元來存放線性表的結點
- 連結串列中結點的邏輯次序和物理次序不一定相同
- 每個結點由:資料域(存放資料資訊)和指標域(存放直接後繼節點地址)兩部分組成
- 注意:
- 鏈式儲存是最常用的儲存方式之一
- 它不僅可用來表示線性表,而且可用來表示各種非線性的資料結構
- 連結串列的結點結構
- data域--存放結點值的資料域
- next域--存放結點的直接後繼的地址(位置)的指標域(鏈域)
- 注意:
- 連結串列通過每個結點的鏈域將線性表的n個結點按其邏輯順序連結在一起的。
- 每個結點只有一個鏈域的連結串列稱為單連結串列(Single Linked List)。
- 單連結串列的表示
- 頭節點和頭指標的區別 1. 連結串列中的第一個節點的儲存位置叫做頭指標 2. 連結串列中的第一個節點前預設的一個節點叫做頭節點 3. 頭指標是連結串列必要元素 4. 頭節點不一定是連結串列的必要元素
5. 連結串列的實現及分析
- 結點類
public class LNode<T> {
//資料域
public T data;
//指標域
public LNode<T> next;
//...略
}
複製程式碼
- 查詢操作
- 程式碼實現
//按序號查詢
public T get(int i) {
//獲取第一個節點元素
LNode node = head.next;
//計數器
int pos = 0;
//遍歷節點,直到節點為空 或者 指向第 i 個節點退出迴圈
while (node != null && pos < i) {
node = node.next; //指向後繼節點
++pos;//計數器加一
}
//判斷是否找到節點
if (node == null || pos > i)
throw new RuntimeException("第 " + i + " 個元素不存在!");
return (T) node.data;
}
//按元素值查詢
public int indexOf(T t) {
//獲取第一個節點元素
LNode node = head.next;
//計數器
int pos = 0;
//判斷查詢的值是否為空
if (t == null) {
//遍歷節點,直到節點為空 或者 節點的資料域為空,退出迴圈
while (node != null) {
if (node.data == null) {
return pos;
}
node = node.next; //指向後繼節點
++pos;//計數器加一
}
} else {
//遍歷節點,直到節點為空 或者 指向值為 t的 節點退出迴圈
while (node != null) {
if (t.equals(node.data)) {
return pos;
}
node = node.next; //指向後繼節點
++pos;//計數器加一
}
}
return -1;
}
複製程式碼
- 時間複雜度:每次查詢都是表頭開始遍歷查詢,所以時間複雜度為:O(n)
- 插入操作
- 程式碼實現
//帶頭結點連結串列插入操作
public void insert(int i, T t) {
LNode p = head;
int pos = -1;
//1. 找到第 (i-1)個節點(位置 i 的前驅節點)
while (p.next != null && pos < i - 1) {
p = p.next;
pos++;
}
//判斷插入位置的合法性
if (p == null || pos > i - 1)
throw new RuntimeException("插入節點的位置不合法!");
//2. 建立一個新的節點
LNode newNode = new LNode(t);
//3.1 新節點的後繼指標指向 原先第 i個節點
newNode.next = p.next;
//3.2 第(i-1)節點 p 的後繼指標指向新節點
p.next = newNode;
}
//不帶頭結點
public void insert(int i, T t) {
LNode p = head;
int pos = 0;
while (p.next != null && pos < i - 1) {
p = p.next;
pos++;
}
//判斷插入位置的合法性
if (p == null || pos > i)
throw new RuntimeException("插入節點的位置不合法!");
//2. 建立一個新的節點
LNode newNode = new LNode(t);
if(i==0){
//插入表頭時
newNode.next = head;
head = newNode;
}else{
newNode.next = p.next;
p.next = newNode;
}
}
複製程式碼
- 時間複雜度:在第i個元素插入結點,需要找到第(i-1)個結點,時間複雜度:O(n)
- 刪除操作
- 程式碼實現
public T remove(int i) {
LNode p = head;
int pos = -1;
//找到待刪除節點的前驅節點
while (p.next != null && pos < i - 1) {
p = p.next;
++pos;
}
if (pos > i - 1 || p == null)
throw new RuntimeException("刪除節點的位置不合法!");
//待刪除節點
LNode remove = p.next;
//3. 第 (i-1) 節點的指標指向 (i+1)節點
p.next = remove.next;
return (T) remove.data;
}
複製程式碼
- 時間複雜度:O(n)
- 單連結串列的建立
- 示意圖
- 頭插入法
public void insertAtHead(T t) {
//構建新插入的節點
LNode newNode = new LNode(t);
//新節點的後繼指標指向頭結點的頭指標
newNode.next = head.next;
//頭指標指向新節點
head.next = newNode;
}
複製程式碼
- 尾插法
public void insertTail(T t) {
//獲取到最後的節點
LNode tail = this.head;
while (tail.next != null) {
tail = tail.next;
}
//構造新的節點
LNode newNode = new LNode(t);
//新節點指標指向 尾節點指標
newNode.next = tail.next;
//尾節點指標指向新節點
tail.next = newNode;
}
複製程式碼
6. 迴圈連結串列
- 實現迴圈連結串列的方式
- 使用頭指標的方式
- 使用尾指標的方式
- 使用頭尾指標的方法
7. 雙向連結串列
- 結點類
public class DuLNode<E> {
public E data;//資料域
public DuLNode<E> prior;//前驅指標
public DuLNode<E> next;//後驅指標
public DuLNode() {
this(null);
}
public DuLNode(E data) {
this.data = data;
this.prior = null;
this.next = null;
}
}
複製程式碼
- 插入操作
public void insert(int i, E t) {
//先判斷索引是否合法
if (i < 0 || i > length)
throw new RuntimeException("插入元素的位置不合法! i=" + i);
DuLNode<E> p = head;
//下標
int index = -1;
//找到第i個元素
while (p.next != null && index < i) {
index++;
p = p.next;
}
//建立一個新的結點
DuLNode<E> newNode = new DuLNode<>(t);
if (i == 0 || i == length) {
newNode.prior = p;
p.next = newNode;
} else {
//1. 第i個結點p的前驅結點的後繼指向新結點
p.prior.next = newNode;
//2. 新結點的前驅指向第(i-1)個結點
newNode.prior = p.prior;
//3. 新結點的後驅指向第i個結點p
newNode.next = p;
//4. 第i個結點p的前驅指向新結點
p.prior = newNode;
}
//長度加一
length++;
}
複製程式碼
- 刪除操作
public E remove(int i) {
//先判斷索引是否合法
if (i < 0 || i > length - 1)
throw new RuntimeException("刪除元素不存在! i=" + i);
DuLNode<E> p = head;
//下標
int index = -1;
//找到第i個元素
while (p.next != null && index < i) {
index++;
p = p.next;
}
DuLNode<E> remove = p;
//1. 第(i-1)個結點的後驅指向 第 (i+1)個結點
p.prior.next = p.next;
//2. 第 (i+1)個結點的前驅指向 第(i-1)個結點
p.next.prior = p.prior;
//長度減一
length--;
return remove.data;
}
複製程式碼
8. 連結串列與順序表的比較
- 基於空間考慮
- 分配方式
- 順序表:靜態分配。
- 連結串列:動態分配。
- 難以估計其儲存規模時,以採用動態連結串列作為儲存結構為好。
- 儲存密度
- 順序表:=1
- 連結串列:<1
- 為了節約儲存空間,宜採用順序表作為儲存結構。
- 儲存密度=(結點資料本身所佔的儲存量)/(結點結構所佔的儲存總量)
- 分配方式
- 基於時間考慮
- 存取方法
- 順序表: 隨機儲存結構,時間複雜度O(1);
- 連結串列:順序存取結構,時間複雜度O(n)
- 操作主要是進行查詢,很少做插入和刪除操作時,採用順序表做儲存結構為宜
- 插入刪除操作
- 在順序表中進行插入和刪除,平均要移動表中近一半的結點,尤其是當每個結點的資訊量較大時,移動結點的時間開銷就相當可觀。
- 在連結串列中的任何位置上進行插入和刪除,都只需要修改指標。
- 存取方法