[從今天開始修煉資料結構]線性表及其實現以及實現有Itertor的ArrayList和LinkedList
一、線性表
1,什麼是線性表
線性表就是零個或多個數據元素的有限序列。線性表中的每個元素只能有零個或一個前驅元素,零個或一個後繼元素。在較複雜的線性表中,一個數據元素可以由若干個資料項組成。比如牽手排隊的小朋友,可以有學號、姓名、性別、出生日期等資料項。
2,線性表的抽象資料型別
線性表的抽象資料型別定義如下。
ADT List Data 線性表的資料物件集合為{a1,a2,...,a3},每個元素的型別均為DataType Operation InitList (L) : 初始化操作,建立一個空的線性表L ListEmpty (L) : 若線性表為空,則返回true,否則返回false ClearList(L): 清空線性表 GetElem(L,i,e): 將線性表L中的第i個位置元素值返回給e LocateElem(L,e): 在L中查詢與給定值e相等的元素,若查詢成功,返回位 序;若查詢失敗,返回0 ListInsert(L,i,e): 在L中的第i個位置插入新元素e ListDelete(L,i,e): 刪除線性表L中的第i個未知元素,並通過e返回其值 ListLength(L): 返回L中的元素個數 endADT
上述操作是最基本的,對於實際中涉及的線性表有更復雜的操作,但一般可以用上述簡單操作的組合來完成,例如我們來思考將線性表A和B組合,實現並集操作,只要迴圈B中的元素,判斷該元素是否在A中,若不存在,插入A即可,實現如下。
public class ListDemo { public static void main(String[] args){ List<String> ListA = new ArrayList<String>(); List<String> ListB = new ArrayList<String>(); ListA.add("zhang"); ListA.add("li"); ListA.add("wang"); ListB.add("zhang"); ListB.add("li"); ListB.add("liu"); for (String name: ListB ) { if (!ListA.contains(name)){ ListA.add(name); } } System.out.println(ListA.toString()); } }
二、線性表的順序儲存結構
1,順序儲存結構的實現
可以用一維陣列來實現順序儲存結構。描述順序儲存結構需要三個屬性:
1,儲存空間的起始位置:即陣列的儲存位置
2,線性表的最大儲存容量:陣列初始化的長度
3,線性表的當前長度
2,順序儲存結構的泛型實現
package List;
import java.util.Iterator;
import java.util.List;
public class ArrayListDemo<T> {
//初始化後可存放的數量,從1開始計數
private int capacity;
//private T[] data; 不能構造泛型陣列,我們用Object[]
private Object[] data;
//已存的數量,從0開始計數。 當size == capacity 時,滿。 size = index + 1.
private int size;
private static int DEFAULT_CAPACITY = 10;
/**
* 自定義順序儲存線性表的建構函式
* @param capacity 陣列的初始化長度
*/
public ArrayListDemo(int capacity) {
//下面寫法會報錯。因為Java中不能使用泛型陣列,有隱含的ClassCastException
//date = new T[length];
//那麼JDK中的ArrayList是怎麼實現的?
//JDK中使用Object[]來初始化了表
if (capacity > 0){
this.capacity = capacity;
this.data = new Object[capacity];
size = 0;
}else {
throw new IndexOutOfBoundsException("長度不合法");
}
}
public ArrayListDemo() {
this(DEFAULT_CAPACITY);
}
public void add(T element){
if (size >= capacity){
grow();
}
data[size++] = element;
}
private void grow() {
capacity = (int)(capacity * 1.5);
Object[] newDataArr = new Object[(int)(capacity)];
for (int i = 0; i < size; i++){
newDataArr[i] = data[size];
}
data = newDataArr;
}
/**
* 指定序號元素的獲取,Java核心技術中稱隨機存取
* @param index 得到陣列元素的角標
* @return 對應的資料元素
*/
public T getElem(int index){
if (index >= size){
throw new IndexOutOfBoundsException();
}
return (T) data[index]; //這裡會報警告,如何解決?
}
public void insertElem(int index, T elem){
if (size == capacity){
grow();
}
for (int i = size - 1; i > index; i--){
data[i + 1] = i;
}
size++;
data[index] = elem;
}
private void checkIndex(int index)throws IndexOutOfBoundsException{
//TODO
if (index < 0){
throw new IndexOutOfBoundsException();
}
}
/**
* 從指定index的位置上移除元素,且返回該元素的值
* @param index 要移除的索引
* @return 被移除的元素的值
* @throws IndexOutOfBoundsException 超出範圍
*/
public T removeElem(int index)throws IndexOutOfBoundsException{
checkIndex(index);
if (index >= size){
throw new IndexOutOfBoundsException();
}else {
Object elem = data[index];
for (int i = index; i < size - 1; i++){
data[i] = data[i + 1];
}
size--;
return (T)elem; //這裡會報警告,如何解決?
}
}
public int size(){
return capacity;
}
public ArrayListItertor itertor(){
return new ArrayListItertor();
}
private class ArrayListItertor implements List {
private int currentIndex;
public ArrayListItertor(){
currentIndex = 0;
}
@Override
public boolean hasNext() {
return !(currentIndex >= size);
}
@Override
public Object next() {
Object o = data[currentIndex];
currentIndex++;
return o;
}
}
}
順序儲存結構的優點:無需為表中元素的邏輯關係而增加額外的儲存空間;可以快速存取表中任意位置的元素
缺點:插入和刪除操作需移動大量元素;當線性表長度變化較大時,難以確定儲存空間容量;造成儲存空間的“碎片”。
三、鏈式儲存結構
1,鏈式儲存結構的概念
為了表示每個資料元素ai與其直接後繼資料元素ai+1之間的邏輯關係,對資料元素ai來說,除了儲存其本身的資訊之外,還需儲存一個指示其直接後繼的資訊(即直接後繼的儲存位置)。把儲存資料元素資訊的域成為資料域,把儲存後繼位置的域成為指標域。
指標域中儲存的資訊稱作指標或鏈,這兩部分資訊組成資料元素ai的儲存映像,稱為結點Node。
連結串列中第一個結點的儲存位置叫做頭指標。
連結串列中最後一個結點指標為“空”。
2,鏈式儲存結構的實現
package List; import java.nio.file.NotDirectoryException; import java.util.Collection; import java.util.Iterator; import java.util.LinkedList; /* 模仿JDK的雙向連結串列實現一個簡單的LinkedList 沒有頭結點 */ public class LinkedListDemo<T> implements Iterable { private Node<T> firstNode; private Node<T> lastNode; private int size; public LinkedListDemo(){ size = 0; } /** * 在表尾插入一個新節點 * @param data 新節點的資料域 */ public void addLast(T data){ Node<T> last = lastNode; Node<T> node = new Node<>(data, last, null); lastNode = node; if (last == null){ firstNode = node; } else{ last.nextNode = node; } size++; } /** * 在表頭插入一個新結點 * @param data 新結點的資料域 */ public void addFirst(T data){ Node<T> first = firstNode; Node<T> node = new Node<T>(data,null, first); firstNode = node; if (first == null){ lastNode = node; }else { first.preNode = node; } size++; } public T get(int index){ checkIndex(index); return search(index).data; } public int size(){ return size; } private Node<T> search(int index){ checkIndex(index); Node<T> pointer = firstNode; for (int i = 0; i < index; i++){ pointer = pointer.nextNode; } return pointer; } /** * 在index前面插入一個元素 * @param index 索引 * @param data 資料域 */ public void insert(int index, T data){ checkIndex(index); if (index == 0){ Node<T> f = firstNode; Node<T> newNode = new Node<>(data, null, f); firstNode = newNode; f.preNode = newNode; }else { Node<T> n = search(index); Node<T> pre = n.preNode; Node<T> newNode1 = new Node<>(data, pre, n); pre.nextNode = newNode1; n.preNode = newNode1; } size++; } /** * 檢查index是否越界.合法的最大index = size - 1. * @param index 要檢查的索引 */ private void checkIndex(int index) { if (index >= size || index < 0){ throw new IndexOutOfBoundsException(); } } /** * 根據結點的次序來刪除結點。 後發現與JDK中的刪除操作不同。 * JDK中LinkedList沒有按照次序插入或刪除的操作,都使用比較資料域是否相同的方法來刪除。 * @param index 結點的次序 * @return 被刪除結點的資料域 */ public T remove(int index){ checkIndex(index); Node<T> pointer = search(index); Node<T> pre = pointer.preNode; Node<T> next = pointer.nextNode; if (firstNode == null) { pre.nextNode = next; }else { pre.nextNode = next; pointer.nextNode = null; } if (next == null){ lastNode = pre; }else { next.preNode = pre; pointer.preNode = null; } size--; return pointer.data; } /** * 清空連結串列,幫助GC回收記憶體。 * 在JDK中,LinkedList實現了Iterator,如果迭代到連結串列的中間,那麼只釋放表頭的話就不會引起GC回收 * 所以要在迴圈中逐一清空每一個結點。 */ public void clear(){ for (Node<T> pointer = firstNode;pointer != null; ){ Node<T> next = pointer.nextNode; pointer.data = null; pointer.preNode = null; pointer.nextNode = null; pointer = next; } size = 0; firstNode = null; lastNode = null; } private static class Node<T>{ T data; Node<T> nextNode; Node<T> preNode; public Node(){ } private Node(T data, Node<T> pre, Node<T> next){ this.data = data; this.preNode = pre; this.nextNode = next; } } public LinkedListItertor itertor(){ return new LinkedListItertor(); } class LinkedListItertor implements Iterator{ private Node<T> currentNode; private int nextIndex; public LinkedListItertor(){ currentNode = firstNode; nextIndex = 0; } @Override public boolean hasNext() { return nextIndex != size; } @Override public T next() { Node<T> node = currentNode; currentNode = currentNode.nextNode; nextIndex++; return node.data; } } }
單鏈表與順序儲存結構對比:
單鏈表 | 順序儲存結構 | |
查詢 | O(n) | O(1) |
插入和刪除 | O(1) | O(n) |
空間效能 |
需要預分配;分大了容易浪費;分小了需要擴容 |
不需要預分配 |
三、靜態連結串列
1,什麼是靜態連結串列?
用陣列描述的連結串列叫做靜態連結串列。該陣列的每一個元素有兩個資料域,data儲存資料,cur儲存後繼結點的角標。該陣列會被建立的大一些,未使用的作為備用連結串列。
該陣列的第一個元素的cur儲存備用連結串列的第一個節點的下標;陣列最後一個元素的cur儲存第一個有數值的元素的下標。如下圖。
若為新建的空連結串列,則如下圖。
2,靜態連結串列的實現
package List; import java.util.List; import java.util.prefs.NodeChangeEvent; public class StaticLinkList <T>{ private ListNode<T>[] list; private static int DEFAULT_CAPACITY = 1000; private int capacity; private int size; // private ListNode firstNode; // private ListNode endNode; // private int capacity; // private int size; private StaticLinkList(int capacity){ this.capacity = capacity; list = new ListNode[capacity]; list[0] = new ListNode<T>(null, 1); list[capacity - 1] = new ListNode<T>(null, 0); size = 0; /* size = 0; this.capacity = capacity; list = new Object[capacity]; firstNode = new ListNode<Integer>(null, 1); list[0] = firstNode; endNode = new ListNode<Integer>(null, 0); list[capacity - 1] = endNode; */ } public int size(){ return capacity; } public StaticLinkList(){ this(DEFAULT_CAPACITY); } public void addLast(T data){ ListNode tail = FindTail(); ListNode<T> newNode = new ListNode<T>(data, 0); list[list[0].cur] = newNode; tail.cur = list[0].cur; synchronize(); size++; } /** * 在index前面插入一個元素 由於是單向連結串列,所以要先取到index上一個元素 * @param index 角標 * @param data 資料 */ public void insert(int index, T data){ ListNode beforeIndexNode = searchPreNode(index); int indexCur = beforeIndexNode.cur; ListNode<T> newNode = new ListNode<T>(data, indexCur); list[list[0].cur] = newNode; beforeIndexNode.cur = list[0].cur; synchronize(); size++; } private void synchronize(){ int i = 1; while(list[i] != null){ i++; } list[0].cur = i; } public T delete(int index){ checkIndex(index); ListNode<T> preNode = searchPreNode(index); ListNode<T> indexNode = list[preNode.cur]; //這行報錯NullPointerException int cur = indexNode.cur; preNode.setCur(indexNode.getCur()); indexNode.cur = 0; T data = indexNode.data; indexNode.data = null; synchronize(); size--; return data; } public void checkIndex(int index){ if (index >= size){ throw new IndexOutOfBoundsException(); } } private ListNode FindTail(){ ListNode tailNode = list[capacity - 1]; while (tailNode.cur != 0){ tailNode = list[tailNode.cur]; } return tailNode; } /** * 拿到index - 1這個結點,才能將新結點插入到index位置上 * @param index 要插入的索引(從0開始) * @return 返回查詢到的結點 */ private ListNode<T> searchPreNode(int index){ ListNode<T> node = list[capacity - 1]; for(int i = 0; i < index; i++){ node = list[node.cur]; } return node; } private class ListNode<T>{ private T data; private int cur; private ListNode(T data, int cur){ this.data = data; this.cur = cur; } public void setData(T data) { this.data = data; } public void setCur(int cur) { this.cur = cur; } private T getData(){ return data; } private int getCur(){ return cur; } } }
3,靜態連結串列的優點:在插入和刪除操作時,只需要修改遊標,不需要移動元素。
缺點:沒有解決連續儲存分配帶來的表長難以確定的問題; 失去了順序儲存結構隨機存取的特性。
在高階語言中不常使用,但可以學習這種解決問題的方法。
四、迴圈連結串列
1,什麼是迴圈連結串列
將單鏈表中終端節點的指標端由空指標改為指向頭結點,就使整個單鏈表形成一個環,這種頭尾相接的單鏈表成為單迴圈連結串列,簡稱迴圈連結串列
迴圈連結串列解決了一個很麻煩的問題,如何從當中一個結點出發,訪問到連結串列的全部結點。迴圈連結串列的結構如下圖所示
單鏈表表尾的判斷是p->next是否為空,而迴圈連結串列是判斷p->next是否為頭結點。
2,改造迴圈連結串列
我們在上面的迴圈連結串列中,訪問表頭的複雜度為O(1) , 訪問表尾的複雜度為O(n)。那麼能不能讓訪問表尾的複雜度也為O(1)呢?
我們把頭指標改為尾指標,如下圖
這樣不管是訪問表頭還是表尾都方便了很多。如果我們要把兩個迴圈連結串列合併,只需要做如下操作。
3,迴圈連結串列的實現
package List;
public class CircularLinkedList<T> {
private Node<T> last;
private Node<T> headNode;
public CircularLinkedList(){
Node<T> node = new Node<>();
headNode = new Node<T>(null, node);
last = new Node<T>(null, headNode);
headNode.nextNode = last;
}
/**
* 新增資料時先呼叫此函式設定last中的data,然後再使用add新增其他元素
* @param data last中的data
*/
public void setLast(T data){
last.data = data;
}
public void add(T data){
Node<T> newNode = new Node<>(data, headNode);
last.nextNode = newNode;
last = newNode;
}
public T deleteFromLast(){
Node<T> lastNode = last;
T data = lastNode.data;
//lastNode.data = null;
Node<T> node = lastNode;
while (node.nextNode != last){
node = node.nextNode;
}
node.nextNode = headNode;
last = node;
lastNode.nextNode = null;
lastNode.data = null;
return data;
}
public boolean combine(CircularLinkedList<T> circularLinkedList){
Node<T> listHeadNode = circularLinkedList.last.nextNode;
last.nextNode = circularLinkedList.last.nextNode.nextNode;
circularLinkedList.last.nextNode = headNode;
listHeadNode.nextNode = null;
last = circularLinkedList.last;
return true;
}
private static class Node<T>{
T data;
Node<T> nextNode;
public Node(){
}
private Node(T data, Node<T> next){
this.data = data;
this.nextNode = next;
}
}
}
雙向連結串列的實現在上面ArrayList的實現中給出。
總結:
我們就線性表的兩大結構做了講述,先將的順序儲存結構,通常用陣列實現;然後是我們的重點,由順序儲存結構的插入和刪除操作不方便,消耗時間大,受固定的儲存空間限制,我們引入了鏈式儲存結構。分為單鏈表、靜態連結串列、迴圈連結串列、雙向連結串列做了講解。
&n