轉載請注明出處:http://www.cnblogs.com/wangyingli/p/5928258.html
上一篇《數據結構與算法(一),概述》中介紹了數據結構的一些基本概念,並分別舉例說明了算法的時間復雜度和空間復雜度的求解方法。這一篇主要介紹線性表。
本節內容:
- 一、基本概念
- 二、順序表
- 三、鏈表
- 1、單向鏈表
- 2、單向循環鏈表
- 3、雙向鏈表
- 4、靜態鏈表
一、基本概念
線性表是具有零個或多個數據元素的有限序列。線性表中數據元素之間的關系是一對一的關系,即除了第一個和最后一個數據元素之外,其它數據元素都是首尾相接的。
線性表的基本特征:
- 第一個數據元素沒有前驅元素;
- 最后一個數據元素沒有后繼元素;
- 其余每個數據元素只有一個前驅元素和一個后繼元素。
抽象數據類型:
線性表一般包括插入、刪除、查找等基本操作。其基於泛型的API接口代碼如下:
public interface List<E> {
//線性表的大小
int size();
//判斷線性表是否為空
boolean isEmpty();
void clear();
//添加新元素
void add(E element);
//在指定位置添加新元素
void add(int index, E element);
//刪除元素
E delete(int index);
//獲取元素
E get(int index);
}
線性表按物理存儲結構的不同可分為順序表(順序存儲)和鏈表(鏈式存儲):
- 順序表(存儲結構連續,數組實現)
- 鏈表(存儲結構上不連續,邏輯上連續)
二、順序表
順序表是在計算機內存中以數組的形式保存的線性表,是指用一組地址連續的存儲單元依次存儲數據元素的線性結構。線性表采用順序存儲的方式存儲就稱之為順序表。
其插入刪除操作如圖所示:

注意:
- 插入操作:移動元素時,要從后往前操作,不能從前往后操作,不然元素會被覆蓋。
- 刪除操作:移動元素時,要從前往后操作。
代碼如下:
import java.util.*;
public class SequenceList<E> implements List<E>, Iterable<E> {
private static final int DEFAULT_CAPACITY = 10;
private int size;
private E[] elements;
@SuppressWarnings("unchecked")
public SequenceList() {
size = 0;
elements = (E[])new Object[DEFAULT_CAPACITY];
}
public int size() { return size;}
public boolean isEmpty(){ return size == 0;}
@SuppressWarnings("unchecked")
public void clear(){
size = 0;
elements = (E[])new Object[DEFAULT_CAPACITY];
}
public void add(E element){
add(size, element);
}
//在index插入element
public void add(int index, E element){
if(size >= elements.length) {
throw new RuntimeException("順序表已滿,無法添加");
}
if(index < 0 || index > size) {
throw new IndexOutOfBoundsException("參數輸入錯誤");
}
for(int i=size; i>index; i--) {
elements[i] = elements[i - 1];
}
elements[index] = element;
size++;
}
//刪除元素
public E delete(int index){
if(isEmpty()) {
throw new RuntimeException("順序表為空,無法刪除");
}
if(index < 0 || index >= size) {
throw new IndexOutOfBoundsException("參數輸入錯誤");
}
E result = elements[index];
for(int i=index; i<size - 1; i++) {
elements[i] = elements[i + 1];
}
size--;
elements[size] = null; //避免對象游離
return result;
}
public E get(int index){
if(index < 0 || index >= size) {
throw new IndexOutOfBoundsException("參數輸入錯誤");
}
return elements[index];
}
@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
int num = 0;
@Override
public E next() {
return elements[num++];
}
@Override
public boolean hasNext() {
return num < size;
}
};
}
public static void main(String[] args) {
SequenceList<Integer> sl = new SequenceList<Integer>();
for(int i=0;i<10;i++) {
sl.add(i);
}
System.out.println("刪除1位置元素:"+sl.delete(1));
sl.add(0,15);
for(int i=0;i<sl.size();i++) {
System.out.print(sl.get(i)+" ");
}
}
}
這里需要注意,由於java中不能直接創建泛型數組,所以在順序表的構造函數中先創建了一個Object的數組,然后將它強轉為泛型數組並使用@SuppressWarnings("unchecked")消除未受檢的警告。若對這點還有什么疑問可以參考我的學習筆記 Effective java筆記(四),泛型 中第25、26條。另外在進行刪除操作時應避免對象游離。
在java中,數組一旦創建其大小不能改變,所以在上面的實現中,為了盡可能的不浪費內存必須事先准確的預估順序表的容量。但現實應用中由於存在很多不確定因素,這往往是不切實際的。這時可使用動態調整數組大小的方法來解決這個問題。代碼如下:
private void resize(int num){
@SuppressWarnings("unchecked")
E[] temp = (E[]) new Object[num];
for(int i=0; i<size; i++) {
temp[i] = elements[i];
}
elements = temp;
}
然后在插入和刪除操作中分別加入判斷語句,來調用這個方法
//在index插入element
public void add(int index, E element){
//當順序表滿時,容量加倍
if(size >= elements.length) {
// throw new RuntimeException("順序表已滿,無法添加");
resize(elements.length*2);
}
if(index < 0 || index > size) {
throw new IndexOutOfBoundsException("參數輸入錯誤");
}
....
}
//刪除元素
public E delete(int index){
....
elements[size] = null;
//當元素數量小於容量的1/4時,容量減半
if(size>0 && size <= elements.length/4) {
resize(elements.length/2);
}
return result;
}
注意:在刪除操作中檢查條件為「順序表的大小是否小於容量的 1/4」,而不是1/2。這樣可以避免在1/2這個零界點處反復進行插入刪除操作時,數組進行頻繁復制。
順序表效率分析:
- 順序表插入和刪除一個元素,最好情況下其時間復雜度(這個元素在最后一個位置)為O(1),最壞情況下其時間復雜度為O(n)。
- 順序表支持隨機訪問,讀取一個元素的時間復雜度為O(1)。
順序表的優缺點:
- 優點:支持隨機訪問
- 缺點:插入和刪除操作需要移動大量的元素,造成存儲空間的碎片。
順序表適合元素個數變化不大,且更多是讀取數據的場合。
三、鏈表
鏈表是一種物理存儲單元上非連續、非順序的存儲結構,數據元素的邏輯順序是通過鏈表中的指針鏈接次序實現的。鏈表由一系列結點組成,每個結點包括兩個部分:一個是存儲數據元素的數據域,另一個是存儲下一個結點地址的指針域。
鏈表根據構造方式的不同可以分為:
- 單向鏈表
- 單向循環鏈表
- 雙向鏈表
1、單向鏈表
單鏈表有帶頭結點和不帶頭結點兩種結構,其結構如下

在帶頭結點的單鏈表中,其第一個結點被稱作頭結點。第一個存放數據元素的結點稱作首元結點,頭結點指向首元結點。頭結點是為了操作的統一與方便而設立的,其一般不放數據(也可存放鏈表的長度、用做監視哨等)。此結點不能計入鏈表長度值。
帶頭結點的單鏈表的優點:
- 在鏈表第一個位置上進行的操作(插入、刪除)和其它位置上的操作一致,無須進行特殊處理;
- 無論鏈表是否為空,head一定不為空,這使得空表和非空表的處理一致。
由於帶頭結點的鏈表更容易操作,這里僅實現帶頭結點的單鏈表
帶頭結點的鏈表插入與刪除示意圖:

代碼如下:
import java.util.*;
public class LinkedList<E> implements List<E>, Iterable<E>{
private Node head;
private int size;
private class Node {
E element;
Node next;
}
LinkedList() {
head = new Node();
}
@Override public int size() { return size;}
@Override public boolean isEmpty() { return size==0;}
@Override public void clear() {
head = new Node();
size = 0;
}
@Override public void add(E element) {
add(0, element);
}
@Override public void add(int index, E element) {
if(index < 0 || index > size)
throw new IndexOutOfBoundsException("參數輸入錯誤");
Node current = location(index);
Node newNode = new Node();
newNode.element = element;
Node node = current.next;
current.next = newNode;
newNode.next = node;
size++;
}
//找到第index個結點前的結點
private Node location(int index){
Node current = head;
for(int i=0; i<index; i++) {
current = current.next;
}
return current;
}
@Override public E get(int index) {
if(index < 0 || index >= size)
throw new IndexOutOfBoundsException("參數輸入錯誤");
return location(index + 1).element;
}
//刪除第index個元素
@Override public E delete(int index) {
if(index < 0 || index >= size)
throw new IndexOutOfBoundsException("參數輸入錯誤");
Node current = location(index);
E element = current.next.element;
current.next = current.next.next;
size--;
return element;
}
@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
Node current = head;
@Override
public E next() {
current = current.next;
return current.element;
}
@Override
public boolean hasNext() {
return current.next != null;
}
};
}
public static void main(String[] args) throws Exception{
LinkedList<Integer> list = new LinkedList<Integer>();
for(int i=0;i<10;i++) {
list.add(i);
}
System.out.println("刪除0位置元素:"+list.delete(0));
list.add(0,15);
for (Integer ele : list ) {
System.out.print(ele + " ");
}
}
}
單鏈表效率分析:
在單鏈表上插入和刪除數據時,首先需要找出插入或刪除元素的位置。對於單鏈表其查找操作的時間復雜度為 O(n),所以
-
鏈表插入和刪除操作的時間復雜度均為 O(n)
-
鏈表讀取操作的時間復雜度為 O(n)
單鏈表優缺點:
- 優點:不需要預先給出數據元素的最大個數,單鏈表插入和刪除操作不需要移動數據元素
- 缺點:不支持隨機讀取,讀取操作的時間復雜度為 \(O(n)\)。
2、單向循環鏈表
將單鏈表中終端結點的指針指向頭結點,使整個單鏈表形成一個環,這種頭尾相接的單鏈表稱為單循環鏈表,簡稱循環鏈表。
對於循環鏈表,為了使空鏈表與非空鏈表處理一致,通常設一個頭結點。如下圖:

循環鏈表和單鏈表的主要差異在於鏈表結束的判斷條件不同,單鏈表為current.next是否為空,而循環鏈表為current.next不等於頭結點。對於循環鏈表的增刪改查操作與單鏈表基本相同,僅僅需要將鏈表結束的條件變成current.next != head即可,這里就不在給出了。
在單鏈表中,我們有了頭結點時,對於最后一個結點的訪問需要 O(n)的時間,因為我們需要將單鏈表全部遍歷一次。哪有沒有可能用 O(1)的時間訪問到終端結點呢?當然可以,我們只需改造一下單鏈表,使用指向終端結點的尾指針來表示循環鏈表,這時訪問開始結點(不是頭結點)和終端結點的操作都為 O(1)。它們的訪問操作分別為end.next.next和end,其中end為指向終端結點的引用。這個設計對兩個循環鏈表的合並特別有用,可以避免遍歷鏈表的時間消耗。如:

合並兩個循環鏈表的代碼:
public Node merge(Node endA, Node endB) {
Node headA = endA.next; //保存A表的頭結點
endA.next = endB.next.next;
endB.next = headA;
return endB;
}
3、雙向鏈表
雙向鏈表是在單鏈表的每個結點中,再設置一個指向其前驅結點的指針域。使得兩個指針域一個指向其前驅結點,一個指向其后繼結點。
雙向鏈表的結點表示:
private class Node {
E element;
Node prior; //指向前驅
Node next;
}
對於雙向鏈表,其空和非空結構如下圖:

雙向鏈表是單鏈表擴展出來的結構,它可以反向遍歷、查找元素,它的很多操作和單鏈表相同,比如求長度size()、查找元素get()。這些操作只涉及一個方向的指針即可。插入和刪除操作時,需要更改兩個指針變量。
插入操作:注意操作順序

在current后插入element的代碼為:
element.prior = current;
element.next = current.next;
current.next.prior = element;
current.next = element;
刪除操作相對比較簡單,刪除current結點的代碼為:
current.prior.next = current.next;
current.next.prior = current.prior;
current = null;
雙向鏈表相對於單鏈表來說占用了更多的空間,但由於其良好的對稱性,使得能夠方便的訪問某個結點的前后結點,提高了算法的時間性能。是用空間換時間的一個典型應用。
4、靜態鏈表
用數組描述的鏈表叫靜態鏈表,它是那些沒有指針和引用的語言,如Basic、Fortran等,實現鏈表的方式。由於現在的高級程序語言,一般都擁有指針或引用,可以使用更靈活的指針或引用來實現動態鏈表,所以對於靜態鏈表僅掌握其算法思想即可。
靜態鏈表的思想:
-
讓數組的每個元素有兩個數據域data和cur組成,其中data用來存放數據元素,cur用來存放元素的后繼在數組中的下標。我們把cur稱為游標。
-
通常把數組中未被使用的位置稱為備用鏈表,而數組的第一個位置(下標為0的位置)的cur存放備用鏈表的第一個結點的下標;數組的最后一個位置的cur則存放第一個有元素的位置的下標,相當於鏈表的頭結點作用。
靜態鏈表狀態圖:

代碼如下:
import java.util.*;
public class StaticList<E> implements List<E>, Iterable<E> {
private static final int DEFAULT_CAPACITY = 100;
private int size;
private Node[] nodes;
private class Node {
E element;
int cur;
}
public StaticList() {
initList();
}
@SuppressWarnings("unchecked")
private void initList() {
size = 0;
//注意這句,不能直接new Node[DEFAULT_CAPACITY],java不允許創建泛型數組
nodes = new StaticList.Node[DEFAULT_CAPACITY];
for(int i=0; i<nodes.length; i++) {
nodes[i] = new Node();
nodes[i].cur = i + 1;
}
nodes[nodes.length - 1].cur = 0;
}
public int size() { return size;}
public boolean isEmpty(){ return size == 0;}
public void clear(){
initList();
}
public void add(E element){
add(0, element);
}
//在index插入element
public void add(int index, E element){
if(index < 0 || index > size) {
throw new IndexOutOfBoundsException("參數輸入錯誤");
}
Node prior = location(index);
int newCur = malloc();
if(newCur == 0) {
throw new RuntimeException("順序表已滿,無法添加");
}
nodes[newCur].element = element;
nodes[newCur].cur = prior.cur;
prior.cur = newCur;
size++;
}
//找到第index個結點前的結點
private Node location(int index){
Node prior = nodes[nodes.length - 1];
for(int i=0; i<index; i++) {
prior = nodes[prior.cur];
}
return prior;
}
//分配空間,若備用鏈表非空,返回分配的結點的下標,否則返回0
private int malloc() {
int i = nodes[0].cur;
if(i != 0) {
nodes[0].cur = nodes[i].cur; //備用鏈表的下一個位置
}
return i;
}
//將下標為k的空閑結點回收到備用鏈表
private void free(int index) {
nodes[index].cur = nodes[0].cur;
nodes[0].cur = index;
}
//刪除元素
public E delete(int index){
if(isEmpty()) {
throw new RuntimeException("順序表為空,無法刪除");
}
if(index < 0 || index >= size) {
throw new IndexOutOfBoundsException("參數輸入錯誤");
}
Node prior = location(index);
int temp = prior.cur; //要刪除元素的下標
prior.cur = nodes[temp].cur;
E result = nodes[temp].element;
nodes[temp].element = null;
size--;
free(temp);
return result;
}
public E get(int index){
if(index < 0 || index >= size) {
throw new IndexOutOfBoundsException("參數輸入錯誤");
}
return location(index + 1).element;
}
@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
int temp = nodes[nodes.length - 1].cur;
@Override
public E next(){
E result = nodes[temp].element;
temp = nodes[temp].cur;
return result;
}
@Override
public boolean hasNext() {
return temp != 0;
}
};
}
//測試
public static void main(String[] args){
StaticList<Integer> sl = new StaticList<Integer>();
for(int i=0;i<10;i++) {
sl.add(i);
}
System.out.println("刪除1位置元素:"+sl.delete(1));
sl.add(1,15);
for(int i=0;i<sl.size();i++) {
System.out.print(sl.get(i)+" ");
}
}
}
為了實現數組空間的循環利用,靜態鏈表將所有未被使用過的及已經被刪除的元素空間用游標鏈成一個備用的鏈表。每當插入時就從備用鏈表上取第一個結點作為待插入的新結點,刪除時將結點回收到備用鏈表中。上面代碼中的malloc()和free()方法分別對應了這兩種操作。靜態鏈表的插入和刪除等操作和單鏈表類似,僅需注意結點的cur為一個int變量,具體操作可以參考上面的代碼。
另外需要注意:靜態鏈表初始化時需要創建一個內部類泛型數組StaticList
nodes = (Node[])new Object[DEFAULT_CAPACITY];
但是在上面的代碼中,使用這種方法運行時會報ClassCastException,解決方法是
nodes = new StaticList.Node[DEFAULT_CAPACITY];
這樣就可以解決這個問題,剩下一個未受檢的警告使用@SuppressWarnings("unchecked")注解消除即可。
靜態鏈表有優缺點:
-
優點:插入刪除操作時,只需要修改游標,無需移動元素
-
缺點:需要事先預估鏈表的容量;不能隨機讀取元素;需要人為的管理數組的分配(類似於管理內存分配),失去了java語言的優點。
總的來說,靜態鏈表是為沒有指針的語言設計的一種實現鏈表的方法,盡管可能用不上,但掌握其設計思想還是很有必要的。
總結一下吧,這節主要介紹了線性表兩種不同結構(順序存儲結構和鏈式存儲結構)的實現方法,它們是其他數據結構的基礎,也是現在企業面試中最常考的數據結構類型之一。下一篇我將總結一下線性表中關於鏈表最常考的面試題,感興趣的可以查看我的下一篇博客 面試題(一),鏈表。
