1. 程式人生 > 實用技巧 >謹本人學習資料結構演算法的經歷記錄

謹本人學習資料結構演算法的經歷記錄

資料結構

儲存方式 只有 陣列(順序儲存) 和 連結串列 (鏈式儲存 )

基本操作就是增刪改查 ,遍歷方式 -> (線性)迭代(for,while迴圈)和(非線性)遞迴 。

1.1 、基本概念

資料

什麼是資料?

  • 描述客觀事務的數值、字元.....,能輸入到計算機並且被計算機處理的各種符號的集合
  • 資料就是資訊在計算機中的表示

資料元素

資料元素就是資料的基本單位

在計算機程式中 ,通常把資料元素作為一個整體進行處理

例 :

​ 描述學生資訊的一條記錄就是一個 資料元素

​ 描述一個座標點的資訊就是一個 資料元素

資料元素通常由若干的 資料項組成

例 :

​ 描述學生資訊中的姓名 、學號 、成績都是資料項

​ 座標點的橫座標 ,縱座標就是資料項

資料物件

一組相同性質的資料元素的集合

例 :

​ 學校中所有學生的集合就是資料物件 。

​ 平面座標系中所有點的集合就是資料物件 。

資料結構

相互之間存在一種或多種特定關係的資料元素的集合

資料結構就是資料元素之間的關係

資料結構分為邏輯結構和物理結構 。

資料的邏輯結果由四種 :

​ 集合 :資料僅僅屬於同一個集合 ,沒有其他相互關係

​ 線性 :描述一對一的關係 。

​ 樹形 :描述一對多的關係 。

​ 圖形 :描述多對多的關係 。

資料的邏輯結構一般採用二元組的形式定義 :

​ 資料結構 = (D,S)

​ D :資料元素的集合

​ S :D (資料元素的集合)中元素之間的關係的集合

例 1 :

​ 二元組 : set = (D,S)

​ D = {01,02,03,04,05,06}

​ S = {}

​ 在 set 集合中 ,資料元素除了屬於同一個集合外,不存在其他的關係 。

​ 這就是集合結構 , 資料和資料之前沒有關係

例 2 :

​ 二元組 : linearity = (D,S)

​ D = {01,02,03,04,05,06}

​ S = {<01,02>, <02,03>, <03,04>, <04,05>, <05,06>}

​ 在資料結構 linearity 中 , 資料結構是有序的。

​ 有一個 被稱為 “第一個” 的資料元素 (元素01),和一個 被稱為 “最後一個” 的資料元素 (元素06),、

​ 除了第一個元素外 ,其他每一個元素都有一個 直接前驅元素 。(即前面都有元素)

​ 除了最後一個元素外 ,其他每一個元素都有一個 直接後繼元素 。(即後面都有元素 )

​ 資料元素之間是 1 對 1 的關係 ,就是線性關係 。

例 3 :

​ 二元組 : tree= (D,S)

​ D = {01,02,03,04,05,06}

​ S = {<01,02> ,<01,03> ,<02,04> ,<02,05> ,<03,06>}

​ 在 tree 資料結構中 ,除了第一個元素 (元素 01 )外 ,每個元素都有並且只有一個 直接前去元素 ,每個元素可以有多個 直接後繼元素

​ 資料元素之間是 1 對 多的關係 ,將之稱為 樹型結構 。

例 4 :

​ 二元組 : graph = (D,S)

​ D = {01,02,03,04,05,06}

​ S = {<01,02> ,<01,03> ,<02,05> ,<05,06> ,<06,02> ,<05,04> ,<04,05>}

​ 在 graph 資料結構中 ,每個元素可以有多個 直接前驅元素 ,每個元素也可以有多個 直接後繼元素 。

​ 這種資料結構的特點 :多對多的關係 ,將其稱為圖形結構

小結 :

​ 資料的物理結構就是邏輯結構在計算機中的儲存表示 。它有兩種表示形式 :

​ 順序儲存 ,鏈式儲存

​ 1、順序儲存 :就是使用一塊連續的儲存空間 ,資料之間緊挨在一起 。資料的前驅與後繼的關係可以通過資料元素在記憶體種相對位置反應出來 。

​ 2、鏈式儲存 :資料元素的儲存位置不是連續的 ,每個元素儲存下一個元素的儲存地址 。

2.1、抽象資料型別

資料型別

一組性質相同的資料的集合及該資料集合上操作的總稱 。

譬如 Java 中 int 型別 ,資料的集合 : -2147483648~2147483647 ,

在這組資料上的操作 :加、減、乘、除、求餘 .....

抽象資料型別

abstract data type ,簡稱 ADT

由一組資料模型及該模型上的一組操作組成 ,

抽象資料型別 ADT ,僅僅討論它的邏輯特性 ,不關心實現 。

抽象資料型別一般使用一個 三元組 表示 :

​ ADT = {D,S,P}

​ D 是 資料物件 。S 是 D 上的關係 。P 是 D 上的操作 。

在定義抽象資料型別 ,可以使用以下的格式 :

ADT 抽象資料型別名 {

資料物件 :<資料物件的定義>

資料關係 :<資料關係的定義>

資料操作 :<基本操作的定義>

}

抽象資料型別可以對應一個 Java 類 :

​ 資料物件與資料關係可以通過類的成員變數來儲存和表示 ,

​ 資料操作可以使用方法;來實現 。

3.1、演算法及效能分析

1.3.1 演算法

演算法就是為解決某一個特定問題而規定的一系列的操作 。是一組有序的指定的集合 。

資料結構與演算法就是一對閨蜜 (即 兩者之間必然有某種直接或間接的聯絡)

譬如 : 求一組數字的累加和 ?

public class JavaArithmeticDemo () {
    public static void main(String[] args) {
        t2(100);
    }
    
    // 四行程式碼
   public static void t1 (int num) {
        int m = 0;
        for (int i = 1; i <= num ;i++) {
            m += i ;
        }
        System.out.println("累加和為 : " + m);
    }
    
    // 高斯演算法
    // sum = 1 + 2 + 3 + 4 + .....+ 100
    // sum = 100 + 99 + 98 + 97 + ...+1
    // 兩倍sum = 101 * 100
    // 兩行程式碼 
    public static void t2(int num) {
        int m = num * (num+1) / 2 ;
        System.out.println("累加和為 : " + m);
    }
}

演算法有五個特性 :

​ 輸入 :一個 演算法 有 0 個或多個 輸入 ;

​ 輸出 :至少有一個 輸出 ;

​ 有窮性 :演算法中執行指令的個數應該是有限的 ,執行有窮的步驟後能結束 ;

​ 確定性 :對於特定的合法輸入 ,它的輸出應該是唯一的 ;

​ 可行性 :演算法能夠實現 ,並且在有限的時間內完成 ;

演算法設計要求

正確性 :沒有語法錯誤 ,對於合法的輸入產生滿足需求的輸出 。對於特定的輸入也能夠產生正確的輸出 。

可讀性 :演算法另一個目的是 為了交流 ,方便閱讀 。

健壯性 :對於不合理的輸入要求 ,也能給出合理的提示資訊 ,而不是程式崩潰 。

時間效率高與儲存空間小 。

評價一個 演算法效能的好壞 。實際上就是評價演算法的資源佔有率 。計算機最重要的資源就是時間和空間 。

使用 時間複雜度 衡量程式執行需要的 時間 。

使用 空間複雜度 衡量程式所佔記憶體的 大小 。

1.3.2 時間複雜度

討論計算機程式執行的時間可以採用以下方法 :

​ 時候統計

​ 程式設計實現這個演算法 ,統計所需要的時間 。

​ 事前分析

​ 採用漸進時間複雜度分析估算 ,

漸進時間複雜度 ,簡稱時間複雜度 ,在進行演算法分析時 ,語句總的執行次數 ,記作 T(n) 。是關於問題規模 n 的函式 ,分析 T(n) 隨著問題規模 n 的變化情況 ,確定 T(n) 的數量級 。

T(n) = O(f(n)) ,表示隨著問題規模 n 的增大 ,演算法執行的時間增長率和 f(n) 函式的增長率相同 , f(n) 是問題規模 n 的一個函式

隨著輸入規模 n 的增大 ,T(n) 增長越慢的演算法越好 。

1.3.3 演算法時間複雜度分析

https://blog.csdn.net/c99463904/article/details/77414163

預估程式碼的基本操作執行次數 。

演算法1、:

​ 計算 1+2+3+4+......+n 的累加和 ,高斯演算法

public static void sum01(int n) {
    int sum = n * (n + 1) / 2 ;
}

順序執行 ,時間複雜度 T(n) = O(1) ,是常數階

演算法2、:

​ 計算 1+2+3+4+......+n 的累加和

public static void sum02(int n) {
    int sum = 0;
    for (int i = ; i <= n ; i++) {
        sum += i ;
    }
}

T(n) = O(n) ,線性階 【增長率和n的規模有關 】

演算法3、:

​ 計算 1+2+3+4+......+n 的累加和

public static void method01(int n) {
    int i = 1;
    int count = 0;
    while(i <= n) {
        i = i * 2;
        count++;
    }
}

迴圈控制變數 i 的值 :1,2,4,8,16 ....2的x次方,當執行了 x 次 ,i 的值為 2的x次方 時 迴圈結束 。

迴圈條件 i <= n ,2的x次方 <= n 不成立時迴圈退出

T(n) = O(logn) n的對數

演算法 4、:

public static void method02(int n) {
    int count = 0;
    int s = 0;
    while(s <= n) {
        count++ ;
        s = s + count;
    }
}

假設迴圈執行 x 次 ,count 變數 在迴圈過程中的值 :0,1,2,3,4,5,。。。。。x

在執行完第 x 次後迴圈結束 ,s <= n 不成立時 ,s的值是

​ s = 0+1+2+3+....x = x*(x+1)/ 2 = (x二次方 + x ) / 2

​ T(n) = O(n的二次方);

演算法5、:

public static void method(int n) {\
    int count = 0;
    for (int i = 1; i<= n; i++) {
        for (int j = 1; j <= n; j++) {
            count ++;
        }
    }
}

T(n) = O(n的二次方)

常見時間複雜度函式的增長率

1.3.4 空間複雜度

為了求解某一問題 ,在執行操作期間所需要的儲存空間大小 。不包含用來儲存輸入所需要的空間 。

記作:

​ S(n) = O(f(n))

結論 :

​ 演算法的空間複雜度是以時間複雜度為上限的 。

1 線性表

1.1 線性表的抽象資料型別

資料結構的四種邏輯結構 :集合 、線性 、樹狀 、網狀 、

linearity = (D,R)

​ D = {a1,a2,a3,a4}

​ R = {<a1,a2>,<a2,a3>,<a3,a4>}

​ a1元素稱為第一個元素 ,其他的元素都有一個 直接前驅元素 。

​ a4元素稱為最後一個元素 ,其他的元素都有一個直接後繼元素

生活中的線性結構 :排隊 ,火車 ,

1.1 線性表的抽象資料型別

​ ADT List {

​ 資料物件 :D = {ai 屬於某個資料物件 ,i=0,1,2,3,4}

​ D={a0,a1,a2,a3,a4 ...an},所有的元素都是同一個資料型別

​ 資料關係 :R={<a1,a1+1>}

​ 資料操作 :

​ getSize() :返回線性表中元素的個數

​ isEmpty() :判斷線性表是否為空

​ insert(i,e) :線上性表中的 i 索引值位置 插入 e 元素

​ contains(e) :線上性表中判斷是否存在元素 e,存在則返回 true

​ indexOf(e) :返回元素 e 線上性表中的索引值 ,不存在則返回 -1

​ remove(e) :刪除線性表第一個與 e 相同的元素 ,刪除成功則返回刪除的元素

​ remove(i) :刪除線性表中指定索引值的元素 ,返回刪除的元素

​ replace(i,e) :把線性表中索引值 為 i 的元素替換為 元素e ,返回舊的元素

​ get(i) :返回線性表中索引值為 i 的元素

​ insertBefore(p,e) :線上性表中元素 p 的前面插入元素e

​ insertAfter(p,e) :線上性表中元素 p 的後面插入元素e

涉及到索引值的地方 ,都要設定索引越界 ,則報錯

}

1.2 List介面

使用 Java 中的介面來表示 ADT 中的資料操作 ,在使用類完成抽象資料型別時 ,只要這個類實現介面即可完成抽象資料型別中 定義的操作 。

package com.yuanwu.ei;

public interface MyList {
	int getSize();		// 返回線性表中元素的個數
	boolean isEmpty();	//判斷是否為空
	void insert(int i ,Object e);	// 線上性表中的 i 索引值位置 插入 e 元素 
	boolean contains(Object e);		// 線上性表中判斷是否存在元素 e,存在則返回 true 
	int indexOf(Object e);			// 返回元素 e 線上性表中的索引值 ,不存在則返回 -1
	Object remove(Object e);		// 刪除線性表第一個與 e 相同的元素 ,刪除成功則返回刪除的元素
	Object remove(int i);			// 刪除線性表中指定索引值的元素 ,返回刪除的元素
	void replace(int i ,Object e);	// 把線性表中索引值 為 i 的元素替換為 元素e ,返回舊的元素
	Object get(int i);				// 返回線性表中索引值為 i 的元素
	boolean insertBefore(Object pObject ,Object e);	// 線上性表中元素 p 的前面插入元素e 
	boolean insertAfter(Object pObject ,Object e);		// 線上性表中元素 p 的後面插入元素e
}

1.3 線性表的順序儲存與實現

1.3.1 插入元素

insert(int i ,Object e) ,i 索引 ,e 元素

1)、需要時 ,可以對陣列擴容

2)、把 i 位置開始的元素依次後移

3)、把要插入的元素儲存到 i 位置

1.3.2 刪除元素

remove(int i)

1)、從 i+1 開始 ,把元素依次前移 ,

2)、把最後一個元素置為 null

1.3.3 具體程式碼的實現

/**
 * 通過陣列實現線性表 
 * */
public class MyArrayList implements MyList {
	private Object[] elements;	//定義陣列儲存資料元素 
	private static final int DEFAULT_CAPACITY = 16;	//定義陣列的初始長度(容量)
	private int size;
	
	// 構造方法 
	public MyArrayList() {
		elements = new Object[DEFAULT_CAPACITY];
	}
	public MyArrayList(int initialCapacity) {
		elements = new Object[initialCapacity];
	}
	
	// 返回元素的個數
	@Override
	public int getSize() {
		return size;
	}

	@Override
	public boolean isEmpty() {
		// 判斷線性表是否為空
		return size == 0;
	}

	@Override
	public void insert(int i, Object e) {
		// 線上性表的 i 位置 插入 元素 e
		// 判斷 索引值 i 是否越界
		if (i <0 || i > size) {
			throw new IndexOutOfBoundsException(i + "越界");
		}
		// 如果陣列容量滿了,對陣列擴容
		if (size >= elements.length) {
			expandSpace();		// 陣列擴容 
		}
		// 從 i 開始 ,把元素依次後移	
		for (int j = size; j > i; j--) {
			//  把 elements[j-1] 的值 賦值給 elements[j]
			//	陣列長度-1的資料 賦值給 當前 陣列索引,實現 元素後移 
			elements[j] = elements[j-1];
		}
		// 把元素e 儲存到 i 位置
		elements[i] = e;
		// 元素個數 +1
		size++;
	}
	
	private void expandSpace() {
		// 定義一個更大的陣列 ,預設2倍擴容陣列 
		Object[] newElements = new Object[elements.length * 2];
		// 讓原來資料的內容複製到新的陣列中
		for (int i = 0; i<elements.length; i++) {
			newElements[i] = elements[i];
		}
		// 讓原來的陣列指向新的陣列
		elements = newElements ;
	}
	
	// 判斷當前線性表中是否包含元素 e
	@Override
	public boolean contains(Object e) {
		return indexOf(e) >= 0;
	}

	// 返回元素e 線上性表中i第一次出現的索引值 ,不存在則返回 -1
	@Override
	public int indexOf(Object e) {
		if (e != null) {
			for (int i = 0; i < size; i++) {
				if (e.equals(elements[i])) {
					return i;
				}
			}
		}
		return -1;
	}
	
	// 刪除 指定元素的 元素 
	@Override
	public Object remove(Object e) {
		// 返回元素e 第一次的出現的索引值 ,
		int index = indexOf(e);
		if (index < 0) {
			throw new IndexOutOfBoundsException("陣列下標越界");
		}
		return remove(index);
	}

	// 刪除指定的索引值發元素 
	@Override
	public Object remove(int i) {
		// 判斷 i 是否越界 
		if (i < 0 || i > size) {
			throw new IndexOutOfBoundsException("陣列下標越界");
		}
		// 儲存刪除的元素
		Object old = elements[i];
		// 把 i+1 開始的元素依次前移 。
		for (int m = 0; m < size - 1; m++) {
			elements[m] = elements[m+1];
		}
		// 把最後的元素位置置為null
		elements[size - 1] = null;
		// 修改元素的個數
		size--;
		return old;
	}

	// 把線性表中索引值 為 i 的元素替換為 元素e ,返回舊的元素
	@Override
	public Object replace(int i, Object e) {
		// 判斷索引值示符越界 
		if (i < 0 || i > size ) {
			throw new IndexOutOfBoundsException("陣列下標越界");
		}
		// 儲存被替換的元素
		Object old = elements[i];
		// 替換 
		elements[i] = e ;
		// 把原來的元素返回
		return old;
	}

	// 返回指定位置的元素
	@Override
	public Object get(int i) {
		if (isIndexBounds(i)) {
			return elements[i];
		}
		return null;
	}
	
	// 判斷索引值是否越界
	public boolean isIndexBounds(int i) {
		if (i < 0 || i > size) {
			throw new IndexOutOfBoundsException("陣列下標越界");
		}
		return true;
	}

	// 在指定的元素前插入一個元素
	@Override
	public boolean insertBefore(Object pObject, Object e) {
		// 確定元素 p 線上性表中的位置
		int index = indexOf(pObject);
		if (index < 0) {
			return false;
		}
		insert(index, e);
		return true;
	}

	// 在指定的元素 後插入一個元素
	@Override
	public boolean insertAfter(Object pObject, Object e) {
		// 確定元素 p 在 線性表中的位置
		int index = indexOf(pObject);
		if (index < 0) {
			return false;
		}
		insert(index + 1, e);
		return true;
	}
	
	@Override
	public String toString() {
		StringBuffer sBuffer = new StringBuffer();
		sBuffer.append("[");
		for (int i = 0; i < size; i++) {
			sBuffer.append(elements[i]);
			if (i < size - 1) {
				sBuffer.append(",");
			}
		}
		sBuffer.append("]");
		return sBuffer.toString();
	}

}

1.3.4 順序儲存的特點

優點 :

​ 順序儲存時使用陣列實現的 ,陣列可以通過索引值快速訪問每個元素

int [] data = new int[4];

data -> 陣列名 實際上是變數名 ,儲存在棧區 指向堆 的儲存地址 指向 [ 地址 0x1234 ]

new 關鍵字 會在 堆 分配一塊儲存空間 。 [ 地址 0x1234 ]

data[2] = 666;

為什麼通過下標就可以訪問陣列元素 ?

​ 通過下標可以計算陣列元素的地址 :

​ data[2] 元素的地址 計算公式 :

​ data + 2 * 4

​ data 是陣列名 ,儲存陣列的起始地址

​ 2 是下標

​ 4 是陣列元素型別所佔的位元組數 ,陣列中儲存的是 int 型別 ,每個元素佔 4個位元組

​ 0x1234 + 2 + 4

缺點 :

​ 在 插入 / 刪除時 , 需要移動元素 ,

​ 而且線性表長度是固定的 ,很難確定儲存容量

應用場景 :

​ 適合 查詢操作 ,

1.4 線性表的鏈式儲存與實現

1.4.1單向連結串列

單向連結串列 ,也稱為單鏈表 ,每個儲存單元至少有兩個儲存域 ,一個用來儲存資料 ,另一個儲存下一個儲存單元的引用 。

各個儲存單元的地址可以是不連續的 。 【一個數據域 和 一個指標域 組成的 就是一個儲存單元】 稱為節點 Node

單鏈表 插入 / 刪除 分析

1.4.2 通過單向連結串列實現線性表

插入難點 。*** 節點指向 ,引用指向 。

// 線上性表中插入元素
Node pNode = head;
for (int x = 1; x < i; x++) {
pNode = pNode.next; // 迴圈8次 ,指向的是 第8個節點
}
// 1、修改 剛插入的節點的 next域 指向
/*

  • 假設 迴圈9次 pNode = head【頭節點】
    • 第一次 pNode.next 【指向第一個節點】
      • 第二次 pNdex.next.next
        • ------------------------------------------------------>>>>
          • pNode.next 指向的是 第8次的下一個 節點引用
            • newNode.next = pNode.next
            • 新節點的 下一個節點的引用 指向 pNode.next 指向的是 第九次的 節點引用
              * 新節點的下一個節點引用 指向 第八個節點的下一個節點引用 【8的下一個節點引用是 9,新節點的下一個節點引用指向9】
              • pNode.next = newNode
                • 第8個節點的下一個節點引用 指向 新節點
              • */
                // 先修改 新節點的next指標域 ,再修改i-1 這個節點的next指標域
                newNode.next = pNode.next; // 新插入的節點的下一個節點引用 指向 pNode的下一個節點引用
                pNode.next = newNode; // 第8個節點的下一個節點引用 指向 新節點
                }
                }
                // 元素 個數 +1
                size++;
                }

實現介面

package com.yuanwu.ei.dao.Impl;

import com.yuanwu.ei.dao.MyList;

/**
 * 通過單向連結串列 實現 線性表 
 * */
public class MySingleLink implements MyList{
	private Node head;	// 頭節點
	private int size;	//儲存元素的個數
	
	// 返回元素個數
	@Override
	public int getSize() {
		return size;
	}

	// 判斷 是否為空 
	@Override
	public boolean isEmpty() {
		return size == 0;
	}

	// 線上性表中插入元素
	@Override
	public void insert(int i, Object e) {
		// 判斷索引是否越界
		if (i < 0 || i > size) {
			throw new IndexOutOfBoundsException("線性表下標越界");
		}
		// 建立節點 
		Node newNode = new Node(e, null);
		// 頭節點 為 null 的情況 ,連結串列不存在,剛剛新增的節點 就是頭節點 。
		if (head == null) {
			head = newNode;
		} else {
		// 在 0 位置插入節點 
			if (i == 0) {
				// 新節點的 next域(指標域) 指向原來的頭節點 
				newNode.next = head;
				// 插入的節點 就是新的頭節點
				head = newNode ;
			} else {
				// 插入節點  ,先找到 i-1 的節點 ,前一個節點 ,後一個節點 。
				// 假設 i=9 x
				Node pNode = head;
				for (int x = 1; x < i; x++) {
					pNode = pNode.next;	// 迴圈8次 ,指向的是  第8個節點
				}
				// 1、修改 剛插入的節點的 next域 指向 
				/*
				 * 假設 迴圈9次	pNode = head【頭節點】 
				 * 第一次 pNode.next		【指向第一個節點】
				 * 第二次 pNdex.next.next
				 * 第三次 pNode.next.next.next 
				 * 假設 newNode  節點 <a,0x112>	8前一個節點 <c,0x111> <-9-> 後一個節點是  10<b,0x113>
				 * 因為 pNode = pNode.next pNode 實際上是 pNode.next ,現在又來一個 next域 
				 * 如果pNode.next 指向的是  第八次迴圈的 pNode   即指向 <c,0x111>
				 * ------------------------------------------------------>>>>
				 * pNode.next	指向的是 第九次的 節點引用 
				 * newNode.next = pNode.next
				 * 新節點的 下一個節點的引用 指向                pNode.next	 指向的是 第九次的 節點引用 
				 * 新節點的下一個節點引用 指向 第八個節點的下一個節點引用 【8的下一個節點引用是 9,新節點的下一個節點引用指向9】
				 * pNode.next = newNode
				 * 第8個節點的下一個節點引用 指向 新節點 
				 * */
				// 先修改 新節點的next指標域 ,再修改i-1 這個節點的next指標域 
				newNode.next = pNode.next;	// 新插入的節點的下一個節點引用 指向 pNode的下一個節點引用
				pNode.next = newNode;		// 第8個節點的下一個節點引用 指向 新節點 
			}
		}
		// 元素 個數 +1
		size++;
	}

	// 判斷線性表中是否包含指定的元素 
	@Override
	public boolean contains(Object e) {
		return indexOf(e) >= 0;
	}

	// 返回 元素e 線上性表中第一次出現的 索引值
	@Override
	public int indexOf(Object e) {
		int i = 0;
		Node pNode = head;
		while (pNode != null) {
			// 如果 e==null 並且 節點的資料域 == null ,返回0
			if (e == null && pNode.data == null) {
				return i;
			} else if (e != null && e.equals(pNode.data)) {
				return i;
			}
			i++;
			pNode = pNode.next;
		}
		return -1;
	}

	// 從線性表中 刪除 第一個與e相同的元素
	@Override
	public Object remove(Object e) {
		// 找到元素e 第一次出現的索引值
		int index = indexOf(e);
		if (index < 0) {
			return null; //元素不存在
		}
		return remove(index);
	}

	// 從線性表中刪除指定索引值的元素
	@Override
	public Object remove(int i) {
		if (i < 0 || i >= size) {
			throw new IndexOutOfBoundsException("線性表下標越界");
		}
		Node pNode = head;
		// 刪除頭節點 
		if (i == 0) {
			head = head.next;
			size--;
			// 返回刪除頭節點的資料
			return pNode.data;
		}
		// 找到 i-1這個節點
		for (int x = 1; x < i; x++) {
			pNode = pNode.next;
		}
		// 儲存刪除節點的資料
		Object old = pNode.next.data;
		// 修改 i-1 next域的指向 i+1 節點 【指向當前節點的下一個節點引用,再下一個節點的引用的節點】
		pNode.next = pNode.next.next;
		size--;
		return old;
	}

	// 把線性表中索引值 為 i 的元素替換為 元素e ,返回舊的元素
	@Override
	public Object replace(int i, Object e) {
		// 判斷是否越界
		checkIndexBounds(i); 
		// 找到 索引值為 i 的元素 
		Node pNode = getNode(i);
		// 儲存原來的資料
		Object old = pNode.data;
		// 替換
		pNode.data = e;
		return old;
	}

	// 返回線性表中 i 索引值的位置
	@Override
	public Object get(int i) {
		checkIndexBounds(i);
		Node pNode = getNode(i); 
		return pNode.data;
	}

	// 在指定元素 pObject 的前面插入 元素 e
	@Override
	public boolean insertBefore(Object pObject, Object e) {
		// 找 pObject 的位置
		int index = indexOf(pObject);
		if (index < 0) {
			return false;	//元素不存在
		}
		insert(index, e);
		return true;
	}

	// 在指定元素 pObject 的後面插入 元素 e
	@Override
	public boolean insertAfter(Object pObject, Object e) {
		// 找 pObject 的位置
		int index = indexOf(pObject);
		if (index < 0) {
			return false;	// 元素並不存在
		}
		insert(index + 1, e);
		return true;
	}
	
	// 定義一個內部類表示單向連結串列中的節點 。
	private class Node{
		Object data;	// 儲存資料
		Node next;		// 下個節點的引用 
		public Node(Object data ,Node next) {
			this.data = data;
			this.next = next;
		}
	}
	
	// 檢查索引值是否越界
	private void checkIndexBounds(int i) {
		if (i < 0 || i >=size) {
			throw new IndexOutOfBoundsException("線性表下標越界");
		}
	}
	
	// 定義一個方法 ,返回 i 索引值的元素
	private Node getNode(int i ) {
		if (i < 0 || i >= size) {
			return null;
		}
		if (i == 0) {
			return head;
		}
		// 找到 i 節點 
		Node pNode = head;
		for (int x = 1; x <= i; x++) {
			pNode = pNode.next;
		}
		return pNode; 
	}

	// 重寫 toString
	@Override
	public String toString() {
		StringBuilder sBuilder = new StringBuilder();
		sBuilder.append("[");
		Node pNode = head;
		while(pNode != null) {
			sBuilder.append(pNode.data);
			if (pNode.next != null) {
				sBuilder.append(",");
			}
			pNode = pNode.next;	// 指標下移
		}
		sBuilder.append("]");
		return sBuilder.toString();
	}
}

1.4.3 雙向連結串列

單向連結串列只能通過一個節點的引用訪問他的後繼節點 ,不餓能訪問前驅節點 ,如果要找某個 節點的前驅節點 ,需要從頭結點開始依次查詢 。

在雙向連結串列中 ,擴充套件了節點的結構 ,每個節點除了儲存資料外 ,通過一個引用指向後繼節點 ,再定義一個引用指向前驅節點 :

雙向連結串列的結構 :

插入 / 刪除

1.4.4 雙向連結串列實現線性表

package com.yuanwu.ei.dao.Impl;

import com.yuanwu.ei.dao.MyList;

public class MyDualLinkedList implements MyList{
	private Node first; // 指向頭節點 
	private Node last;	// 指向尾節點 
	private int size ;
	
	// 返回元素的個數 
	@Override
	public int getSize() {
		return size;
	}

	// 判斷 線性表是否為空 
	@Override
	public boolean isEmpty() {
		return size == 0;
	}

	// 在指定的索引值 插入元素
	@Override
	public void insert(int i, Object e) {
		// 檢索索引值是否越界
		if (i < 0 || i > size) {
			throw new  IndexOutOfBoundsException(i + "下標越界");
		}	
		// 2、如果 i==0 ,z在頭部新增元素
		if (i == 0) {
			addFirst(e);
		} else if (i== size) {
			// 3、 如果 i==size,在尾部新增元素
			addlast(e);
		} else {
			// 4、找到 i 節點 ,在i節點的前面插入元素
			Node pNode = getNode(i);
			Node prevNode = pNode.prev;
			// 生成新的節點 
			Node newNode = new Node(e, prevNode, pNode);
			// 修改 後繼節點 
			prevNode.next = newNode;
			// 修改 前驅節點 
			pNode.prev = newNode;
			// 節點個數 +1 
			size++;
		}
	}

	// 返回索引值對應的節點
	public Node getNode(int i) {
		Node pNode = first;	// 賦值? pNode 指向 頭節點 
		/*
		 * 假設 i=4   x 迴圈 0 1 2 3  x<4
		 * 第一次迴圈 pNode = pNode.next	頭節點的下一個節點的引用 
		 * 1、比如 	頭節點的下一個節點的引用是1 ,
		 * 2、但現在有一個插入進來 ,就表示  pNode的下一個節點的引用 指向 1 ,
		 * 3、pNode 節點 在 1節點的前面了,實現 插入節點
		 * 第二次迴圈 pNode = pNode.next.next
		 * 第三次迴圈 pNode = pNode.next.next.next
		 * .......
		 * */
		for (int x = 0; x < i; x++) {
			pNode = pNode.next;	// 指向 ?	pNode  指向 第 i 個節點  【的下一個節點引用】
		}
		return pNode;
	}

	private void addlast(Object e) {
		// 儲存原來的尾節點 ,【指向尾節點】
		Node pNode = last;
		// 生成一個新的節點
		Node newNode = new Node(e,last,null);
		if (pNode == null) {
			first = newNode;
		} else {
			pNode.next = newNode ;
		}
		// 尾節點 指標後移 
		last = newNode;
		size++;
	}

	public void addFirst(Object e) {
		// 儲存原來的頭節點 
		Node pNode = first;
		// 建立一個新節點 
		Node newNode = new Node(e,null,first);
		// 原來的頭節點 指向新節點	,頭節點 ,指標前移 
		first = newNode;
		// 判斷 原來的頭節點 == null  為空
		// 如果空 ,尾節點 指向新節點
		if (pNode == null) {
			last = newNode;
		} else {
			// 不為空 ,原來的頭節點 的前驅節點(上一個節點的引用), 指向 新的節點 
			pNode.prev = newNode;
		}
		// 節點個數 +1
		size++;
	}

	// 判斷 連結串列中 是否包含指定的元素e ,如果存在返回true
	@Override
	public boolean contains(Object e) {
		return indexOf(e) >= 0;
	}

	// 判斷元素e 在連結串列中第一次出現的位置,不存在則返回 -1
	@Override
	public int indexOf(Object e) {
		int count = 0;	//儲存元素e 的索引值
		// 依次遍歷連結串列中的節點 ,比較 節點元素 與 指定e 是否一樣 
		if (e == null) {	// 如果 元素 e 為空
			// for迴圈 	從頭節點開始遍歷 ,迴圈條件 pNode != null	,每迴圈一次 pNode指向下一節點的引用
			for (Node pNode = first; pNode != null; pNode = pNode.next) {
				if (pNode.data == null) {
					return count;
				} 
				count++; //	索引值+1
			} 
		} else {
			for (Node pNode = first; pNode != null; pNode = pNode.next) {
				if (e.equals(pNode.data)) {
					return count;
				}
				count++; //	索引值+1
			}
		}
		
		return -1;
	}

	// 從連結串列中刪除指定的元素 ,並返回刪除的元素
	@Override
	public Object remove(Object e) {
		// 找到元素e 的索引值
		int index = indexOf(e);
		if (index < 0) {
			return null;
		}
		return remove(index);
	}

	// 從連結串列中刪除指定索引值的元素 ,並返回刪除的元素 
	@Override
	public Object remove(int i) {
		// 是否越界
		if (i < 0 | i > size) {
			throw new IndexOutOfBoundsException(i + "下標越界");
		}
		// 找到 索引i 對應的節點 
		Node pNode = getNode(i);
		// 第 i 個 節點  ,第8個節點
		Node prevNode = pNode.prev;	// prevNode 指向 第i個節點的前驅節點	8的前驅節點 是 7	prevNode 指向 7的後繼節點
		Node nextNode = pNode.next;	// nextNode 指向 第i個節點的後繼節點	8的後繼節點 是 9	nextNode 指向 9的前驅節點 
		
		// 刪除頭節點 	
		if (prevNode == null) {
			// 
			// 頭節點 指向 第 i 個節點的下一個節點引用 
			first = nextNode;
		} else {
			// i-1節點的下一個節點引用 指向 i+1節點 
			// i-1節點的後繼節點 指向 i+1節點的前驅節點 
			prevNode.next = nextNode;
		}
		// 刪除尾節點 
		if (nextNode == null) {
			// 尾節點 指向 i-1 節點 
			last = prevNode;
		} else {
			// i+1節點的上一個節點引用 指向 i+1 節點
			// i+1節點的前驅節點 指向 i-1節點的 後繼節點 .
			nextNode.prev = prevNode;
		}
		// 元素個數 -1
		size--;
		return pNode.data;
	}

	// 替換
	@Override
	public Object replace(int i, Object e) {
		// 檢索是否越界
		checkIndexBounds(i);
		// 找到索引值 i 的節點 
		Node pNode = getNode(i);
		// 儲存替換之前的data
		Object oldData = pNode.data;
		pNode.data = e;
		return oldData;
	}

	// 返回指定索引的元素
	@Override
	public Object get(int i) {
		// 檢索是否越界
		checkIndexBounds(i);
		// 找到 索引值為 i 的節點 
		Node pNode = getNode(i);
		return pNode.data;
	}

	// 在指定元素前面插入節點
	@Override
	public boolean insertBefore(Object pObject, Object e) {
		// 獲取指定元素的索引值 
		int count = indexOf(pObject);
		if (count < 0) {	// 連結串列中 不存在 pObject
			return false;
		}
		insert(count, e);
		return true;
	}

	// 在指定元素後面插入節點 
	@Override
	public boolean insertAfter(Object pObject, Object e) {
		// 獲取指定元素的索引值 
		int count = indexOf(pObject);
		if (count < 0) {	// 連結串列中 不存在 pObject
			return false;
		}
		insert(count + 1, e);
		return true;
	}
	
	public class Node{
		Object data;
		Node prev;	//指向前驅節點
		Node next;	//指向後繼節點 
		public Node(Object data, Node prev, Node next) {
			super();
			this.data = data;
			this.prev = prev;
			this.next = next;
		}
	}
	@Override
	public String toString() {
		StringBuilder sBuilder = new StringBuilder();
		sBuilder.append("[");
		for (Node node = first ; node != null; node = node.next) {
			sBuilder.append(node.data);
			if (node != last) {
				sBuilder.append(",");
			}
		}
		sBuilder.append("]");
		return sBuilder.toString();
	}
	
	private void checkIndexBounds(int i) {
		if (i < 0 || i >= size) {
			throw new IndexOutOfBoundsException(i + "下標越界");
		}
	}

}

1.5 順序儲存與鏈式儲存實現線性表的比較

1.5.1 時間上的比較

線性表的基本操作 :查詢 、插入 、刪除

查詢 :

​ 陣列順序儲存 ,直接通過索引值訪問每個元素 ,實現了陣列元素的隨機訪問

​ 連結串列鏈式儲存 ,每次從頭節點或者尾節點開始依次查詢

如果線性表主要是用於查詢操作 ,優先選擇順序儲存的線性表

插入 / 刪除

​ 陣列順序儲存實現的線性表 ,在插入 / 刪除時 ,需要移動大量的元素

​ 連結串列鏈式儲存 ,只需要修改節點的前驅後繼指標指向即可 ,不需要移動元素 。

如果線性表經常用於 插入 /刪除 操作 ,優先選擇鏈式儲存實現的線性表 。

1.5.2 空間比較

順序儲存 :預先分配一塊連續的儲存空間 ,在使用過程中會出現閒置的 空間

鏈式儲存 :儲存空間是動態的 ,不會浪費空間

如果線性表的長度經常變化 ,優先 選擇鏈式儲存

如果 線性表的長度變化不大時 ,優先選擇順序儲存 。因為鏈式儲存需要分配額外的空間儲存節點的前驅和後繼 。


2 棧與佇列

棧與佇列 ,從邏輯結構上看 ,也是線性結構 ,是操作受限的線性結構

2.1 棧

2.1.1 棧的特點及抽象資料型別

​ 棧 (Stack) ,也稱為堆疊 ,是一種操作受限的線性表 ,棧只允許線性表的一端進行插入 / 刪除 等操作 ,不允許在其他位置 插入 / 刪除 。

線上性表中進行插入 / 刪除 的一端稱為棧頂 (top) ,棧頂儲存的元素稱為棧頂元素 ,相對的另一端稱為棧底 (button)

如果棧中沒有資料元素稱為空棧 。

向棧中插入元素 ,稱為進站或入棧 ,從棧中刪除元素稱為退棧或出棧 ,

棧的插入 / 刪除操作只允許 在棧頂進行 ,後進炸的元素 必定 先出棧 ,稱為 “ 先進後出” 表

(First In last Out ,簡稱 FILO ,先進後出) ,

堆疊抽象資料型別的定義 :

ADT Stack {

​ 資料物件 :D={a0,a1,a2...an, ai 是同一種資料型別的元素}

​ 資料關係 :R={<ai,ai+1>}

​ 基本操作 :

​ getSize() 返回元素的個數

​ isEmpty() 判斷堆疊是否為空

​ push(Object) 壓棧 ,入棧

​ pop() 彈棧 ,出棧

​ peek() 返回棧頂元素

}ADT Stack

2.1.2 棧的順序實現

介面 :

public interface MyStack {

	// 返回元素個數
	int getSize();
	// 判斷 棧是否為空
	boolean isEmpty();
	// 壓棧
	void push(Object e) ;
	// 彈棧
	Object pop();
	// 返回棧頂元素 
	Object peek();
	
}

實現 :

public class MyArrayStack implements MyStack{
	private Object[] elements;
	// 堆疊 初始化容量
	private static final int DEFAULT_CAPACITY = 16;
	// 定義一個棧頂指標 
	private int top ;
	
	// 預設初始化 
	public MyArrayStack() {
		elements = new Object[DEFAULT_CAPACITY];
	}
	
	// 指定容量初始化 
	public MyArrayStack(int initialCapacity) {
		elements = new Object[initialCapacity];
	}
	
	// 返回元素個數 
	@Override
	public int getSize() {
		return top;
	}

	// 判斷棧是否為空 
	@Override
	public boolean isEmpty() {
		return top <= 0;
	}

	// 壓棧 
	@Override
	public void push(Object e) {
		// 判斷堆疊是否已滿 ,陣列擴容 
		if (top >= elements.length) {
			// 定義一個更大的陣列
			Object[] newData = new Object[elements.length * 2];
			// 把原來的陣列內容複製到大的陣列中
			for (int i = 0; i< top; i++) {
				newData[i] = elements[i];
			}
			// 讓原來的陣列名指向新的陣列 
			elements = newData;
		}
		// 把元素儲存到棧頂指標指向的位置
		elements[top] = e;
		// 棧頂指標上移
		top++;
	}

	// 彈棧 
	@Override
	public Object pop() {
		// 判斷堆疊是否已空 
		if (top <= 0) {
			throw new StackOverflowError("棧已空 ");
		}
		top--;	// 棧頂指標向下移 
		return elements[top];
	}

	// 返回棧頂元素 
	@Override
	public Object peek() {
		return elements[top];
	}
	
	// 重寫 toString
	@Override
	public String toString() {
		StringBuilder sBuilder = new StringBuilder();
		sBuilder.append("[");
		for (int i = top-1; i >= 0; i--) {
			sBuilder.append(elements[i]);
			if (i > 0) {
				sBuilder.append(",");
			}
		}
		sBuilder.append("]");
		return sBuilder.toString();
	}

}

2.1.3 棧的鏈式實現

使用連結串列作為棧的儲存結構 ,也稱為鏈棧

實現 :

public class MyLinkStack implements MyStack{
	private Node top;	// 儲存棧頂的引用 
	private int size;	// 儲存棧堆元素的個數
	
	// 返回堆疊元素的個數
	@Override
	public int getSize() {
		return size;
	}

	// 判斷堆疊是否為空 
	@Override
	public boolean isEmpty() {
		return size == 0;
	}

	// 壓棧 
	@Override
	public void push(Object e) {
		// 根據 元素生成節點 ,插入到 連結串列的頭部
		Node pNode = new Node(e, top);
		// 修改棧頂元素指向新的節點 
		top = pNode;
		size++;	//棧頂元素個數+1
	}

	// 彈棧 
	@Override
	public Object pop() {
		if (size < 1) {
			throw new StackOverflowError("棧已空");
		}
		Object  OldNode = top.data;	// 儲存 原來的棧頂元素 
		// 棧頂指標 指向 棧頂元素的下一個節點引用 
		// 比如  1 2 3 ,刪除1,top的下一個節點引用 就是 2 ,棧頂指標 指向 2 
		top = top.next;		// 指標下移 
		size--;
		return OldNode;
	}

	// 返回棧頂元素 
	@Override
	public Object peek() {
		// 判斷 棧是否為空 
		if (size < 1) {
			throw new StackOverflowError("棧已空");
		}
		return top.data;
	}
	
	public class Node{
		private Object data;
		private Node next;
		public Node(Object data, Node next) {
			super();
			this.data = data;
			this.next = next;
		}
	}
	
	@Override
	public String toString() {
		StringBuilder sBuilder = new StringBuilder();
		sBuilder.append("[");
		/*
		 * top 賦值給 pNode 指向 棧頂 ,迴圈條件 pNode 不為空 ,pNode 指向 棧頂的下一個節點指向  pNode 所代表的就是當前棧元素 
		 * */
		for (Node pNode = top; pNode != null; pNode = pNode.next) {
			sBuilder.append(pNode.data);
			if (pNode.next != null) {
				sBuilder.append(",");
			}
		}
		sBuilder.append("]");
		return sBuilder.toString();
	}
	
}

2.1.4 棧的應用

​ 棧的特點 :先進後出

1 、進位制轉換

public class TestBaseConversion {

	public static void main(String[] args) {
		System.out.println(convert(100, 2));
	}
	
	public static String convert(int num ,int decimal) {
		/*
		 * 取餘
		 * 除以二
		 * 把 取餘的值 壓棧 
		 * */
		MyArrayStack stack = new MyArrayStack();
		int i = 1;
		int remainder = num % decimal;	// 餘數
		while(num != 0) {
//			int remainder = num % decimal;
//			num = num / decimal;
			stack.push(remainder);	//餘數壓棧
			num = num / decimal;
			remainder = num % decimal;
		}
		
		// 出棧 ,餘數倒敘
		StringBuilder sBuilder = new StringBuilder();
		while(!stack.isEmpty()) {
			sBuilder.append(stack.pop());
		}
		return sBuilder.toString();
	}

}

2 、檢測表示式中括弧是否匹配

​ 假設表示式中包含三種括弧 :小括弧() 、中括弧[] 、大括弧 {} 。這三種括弧可以任意巢狀。

​ (3+5) * [3-6] - {23/4} + ([{}])

​ 對於任意一個左括弧都需要有一個相應 的有括弧匹配 。

​ 最早出現的右括弧應該與最早出現的左括弧匹配 ,【符合 棧 的特點 】

​ 演算法:

​ 讀取整個表示式 ,如果是左括弧 就直接入棧 ,等待與它對應的右括弧出現 ;

​ 如果是右括弧 ,則與當前棧頂的左括弧判斷是否匹配 ,

​ 如果不匹配 ,說明表示式 不合法 。

​ 如果是右括弧 ,棧已空 ,表示不合法 。

​ 讀取完整個表示式 ,棧堆不空 ,表示右左括弧沒匹配上 ,表示式不合法 ;

​ 讀完整個表示式 ,棧是空的表示所有的括弧都能匹配上 。

實現 :

public class TestBracketMatch {

	public static void main(String[] args) {
		String e ="({[]})";
		boolean a = bracketMatch(e);
		System.out.println(a);		// true
	}
	
	// 檢測expression表示式 中的括弧是否匹配 
	public static boolean bracketMatch(String expression) {
		MyArrayStack stack = new MyArrayStack();
		// 遍歷整個表示式 ,如果是左括弧就入棧 ,如果是右括弧 ,就出棧進行判斷是否匹配
		for (int i = 0; i < expression.length(); i++) {
			// 取出表示式的每個字元 
			char cc = expression.charAt(i);
			switch (cc) {
			case '(':
			case '[':
			case '{':
				stack.push(cc);	//左括弧入棧 
				break;
			case '}' :
				if (!stack.isEmpty() && stack.pop().equals('{')) {
					break;
				} else {
					return false;
				}
			case ']' :
				if (!stack.isEmpty() && stack.pop().equals('[')) {
					break;
				} else {
					return false;
				}
			case ')' :
				if (!stack.isEmpty() && stack.pop().equals('(')) {
					break;
				} else {
					return false;
				}
			}
		}
		// 表示式遍歷完後 ,如果棧式空的,表示括弧匹配 
		if (!stack.isEmpty()) {
			return false;
		}
		return true;
	}

}

3 、算術表示式的求值

略 ....... 棧的應用

遇到將運算元 壓入 棧 ,如果 操作符棧 為空 ,把第一個遇到的操作符 壓入棧 ,繼續迴圈

遇到 運算元 壓入棧 ,如果 再一次遇到 操作符 ,並且 操作符棧不為空 ,就將 操作符棧中的操作符彈棧 ,

1、跟 遇到的操作符進行優先順序比較 ,如果 遇到的操作符的優先順序 高於 操作符棧操作符的優先順序 ,將 遇到的操作符壓入操作符棧 。

繼續 迴圈遍歷 。

2、如果 當前運算子的優先順序等於 棧頂運算子的優先順序, 只有一種 情況, 左一半小括弧遇到右一半小括弧的情況

將 左括弧出棧

3、如果上面兩種情況都不滿足 ,即遇到的操作符的優先順序 低於 操作符棧操作符的優先順序

取出兩個運算元棧的 數 取出操作符棧 棧頂的運算子 ,進行計算 。然後將 結果 壓棧

4、第三步計算完 ,沒遍歷完 。如果 操作符棧為空 ,遇到的操作符壓入棧

5、 當 表示式遍歷完 ,操作符棧 不為空 ,說明還沒計算完 ,取出操作符棧的運算子 ,進行運算 。

將結果 壓棧 。

2.2 佇列

佇列的特點及抽象資料型別

佇列 (Queue)簡稱為隊 ,也是一種受限的線性表 ,只允許線上性表的一端進行插入 ,在表的另一端進行刪除 。

在插入資料的一端稱為隊尾(rear)在刪除資料的一端稱為隊首(front)

​ 向佇列新增資料稱為入隊或進隊 ,新入隊的元素稱為隊尾元素 。

​ 在佇列中刪除元素稱為出隊或離隊 ,

​ 元素出隊之後 ,它的後續元素稱為新的隊首元素 。

佇列是一種先進先出(First In First Out 簡稱FIFO)表

佇列抽象資料型別的定義

​ ADT Queue{

​ 資料物件 :D={a0,a1,a2,a3 ....an ,ai 是同一種資料型別的元素}

​ 資料關係 :R = {<ai,ai+1}>

​ 資料操作:

​ getSize() :返回元素的個數

​ isEmpty() :判斷佇列是否為空

​ enQueue(e) :入隊

​ deQueue() :出隊

​ peek() :返回隊首的元素

​ } ADT Queue


佇列的順序儲存實現

在佇列的實現中 ,可以把資料設想為一個圓環 ,這種陣列稱為迴圈陣列 ,用迴圈陣列實現的佇列稱為迴圈佇列 。

用 front 指標指向 隊首元素所在的單元 ,使用 rear 指標指向隊尾元素所在單元的後一個單元 。

在元素入隊時 ,將新入隊的元素儲存到 rear 指向的單元 ,然後 rear 指標後移 ;在出隊時 ,將隊首指標 front 指向的元返回 。並且 front 指標指向後移 ;

如何表示佇列為空 ?

隊首元素出隊 ,front 隊首指標後移 ,如果 front 隊首指標後移 到 rear隊尾指標所在的單元,即佇列為空 。

如何表示佇列已滿 ?

入隊 ,rear 隊尾指標 指向隊尾元素所在單元的下一個單元 ;即入隊,rear 指標後移 。如果 rear指標後移到 front 隊首指標所在單元 ,即佇列已滿

**一般情況下 ,採用 下兩種fang'shi表示佇列已滿 **

1)、少用一個儲存單元 ,當隊尾指標 rear 的下一個單元是隊首指標 front 時 ,停止入隊 ;

​ 即 (rear + 1) % capacity == front 時表示佇列滿 ,隊尾指標所在單元 +1 對 容量 取餘 == fornt

​ 當 front == rear 時 表示佇列已空 。

2)、增設一個標誌表示佇列為空還是已滿 ,通常用 size 變數 表示元素的個數 ,當 sizze == 0時佇列已空 當 size == capacity 時 佇列已滿 。

實現 :

/**
 * 佇列的順序儲存實現
 * */
public class MyArrayQueue {

	private Object[] elements;	// 定義一個數組
	private static final int DEFAULT_CAPACITY = 8;	//定義陣列容量初始大小
	private int front ;	// 隊首 ==在刪除資料的一端稱為隊首(front)==
	private int rear ;	// 隊尾 ==在插入資料的一端稱為隊尾(rear)==
	private int size ;	// 儲存元素個數
	
	public MyArrayQueue() {
		super();
		elements = new Object[DEFAULT_CAPACITY];
	}
	public MyArrayQueue(int initialCapacity) {
		elements = new Object[initialCapacity];
	}
	
	// 返回元素的個數 
	public int getSize() {
		return size;
	}
	
	// 判斷佇列是否為空 
	public boolean isEmpty() {
		return size == 0;
	}
	
	// 入隊
	public void enQueue(Object e) {
		// 如果佇列已滿 ,可以對陣列擴容
		if (size >= elements.length) {
			expandQueue();
		}
		
		elements[rear] = e;	// 把元素e儲存到 rear 指標指向的單元 
		/*
		 * 第一次入隊的時候 ,rear指標指向 0	新增完元素 
		 * rear 指標  = rear + 1 對 陣列長度取餘 ,
		 * 假設 陣列長度 為 8	rear = (0+1) % 8  = 1	rear = (1+1) % 8  = 2	rear = (2+1) % 8  = 3
		 * */
		rear = (rear+1) % elements.length;	// 取餘 rear 指標後移 
		size++;
	}
	
	// 佇列陣列擴容
	private void expandQueue() {
		// 定義一個更大的陣列
		Object[] newData = new Object[DEFAULT_CAPACITY * 2];
		// 把原來的陣列內容複製到新的陣列中
		for (int i = 0; i< size; i++) {
            /*
			 * 刪除的一端 是隊頭  front  
			 * 所以 先複製到 新陣列中 
			 * 然後 front 指標後移 ,第一次對頭 指標是0,第二次 是 1 第三次 是 2
			 * 假設 初始化的時候 ,隊首和隊頭 都處在 0這個位置 ,
			 * 佇列可以看作是一個線性表的 環形資料結構 
			 * 插入資料的時候 (隊尾rear),會隨著 陣列長度變化而變化 ,插入資料 ,rear指標 在當前單元的基礎上 +1 移動 ,
			 * 刪除資料的時候(隊首front),會隨著陣列長度變化而變化 ,刪除資料 ,front 指標指向第二個插入資料的單元 , 從0索引值 向 陣列長度的索引值變化 
			 * */
			newData[i] = elements[front];	
			front = (front + 1) % elements.length;
		}
		// 讓原來的陣列變數指向新的陣列
		elements = newData;
		// 調整新的隊首與隊尾 指標 
		front = 0;
		rear = size;
	}
	// 出隊 
	public Object deQueue() {
		// 如果佇列為空
		if (size <= 0) {
			//丟擲佇列為空異常
			throw new QueueEmptyException("佇列為空");
		}
		// 佇列不為空 ,把front指向的元素返回 ,front指標後移 
		Object old = elements[front];	// 儲存出隊的元素
		/*
		 	假設佇列中已經入隊5個元素 ,此時要出隊 
		 	即 front = (5+1) % 8 = 6
		 * */
		front = (front + 1) % elements.length;	// front指標後移
		size--;
		return old;
	}
	
	// 返回隊首元素
	public Object peek() {
		// 佇列為空 丟擲異常
		if (size <= 0) {
			throw new QueueEmptyException("佇列為空");
		}
		return elements[front];
	}
}

佇列的鏈式儲存實現

使用單向連結串列來實現佇列

把連結串列的頭部作為隊首 ,把連結串列的尾部作為隊尾

每次插入 / 刪除 元素 ,隊首 / 隊尾 指標都要重新指向

實現 :

/**
 * 佇列的鏈式儲存 
 * */
public class MyLinkQueue {
	private Node front;		// 隊首 ==在刪除資料的一端稱為隊首(front)==
	private Node rear;		// 隊尾 ==在插入資料的一端稱為隊尾(rear)==
	private int size;		// 元素的個數 
	
	// 返回元素的個數 
	public int getSize() {
		return size;
	}
	
	// 判斷佇列是否為空
	public boolean isEmpty() {
		return size == 0;
	}
	
	// 入隊 
	public void enQueue(Object e) {
		// 根據新增的元素 生成一個節點 
		Node newNode = new Node(e,null);
		// 把節點 連線到佇列中 
		if (rear == null) {
			// 隊尾為空 ,說明是第一個新增的元素 ,
			// 即是頭節點 ,也是尾節點 。
			rear = newNode;
			front = newNode;
		} else {
			// 把節點連結到佇列的尾部 
			rear.next = newNode;	// 當前隊尾的下一個節點引用 指向新節點 
			rear = newNode;	     // 隊尾rear 指標指向新新增的元素 。新新增的元素就是隊尾
		}
		size++;
	}
	
	// 出隊 
	public Object deueue() {
		// 判斷佇列是否為空
		if (size < 0) {
			throw new QueueEmptyException("佇列為空 ");
		}
		// 儲存 出隊的元素
		Object old = front.element;
		// 調整隊首指標指向 指標指向 當前隊首的下一個節點引用 
		front = front.next;
		// 如出隊後 ,佇列為空 調整尾指標 。
		if (front == null) {
			rear = null;
		}
		size--;
		return old;
	}
	
	// 返回隊首元素
	public Object peek() {
		if (size <=0 ) {
			throw new QueueEmptyException("佇列為空");
		}
		return front.element;
	}
	
	// 通過內部類表示單向連結串列的節點 
	private class Node{
		Object element;
		Node next;
		public Node(Object element, Node next) {
			super();
			this.element = element;
			this.next = next;
		}
	}
	
}

3樹

3.1 樹的定義

樹是由一個集合及該集合上定義的一種關係構成的 ,集合中的元素稱為樹的節點 ,定義的關係稱為父子關係 。父子關係的樹的節點之間建立一個層次結構 。

樹的遞迴定義 :

​ 樹(Tree)是由 n(n>=0) 個節點組成的有限集 ,當 n = 0是 ,稱為空樹 ,不包含任何節點 ;當 n > 0是 就是一顆非空樹 ,

​ 1、有且僅有特定的稱為根的節點(root)。

​ 2、當 n > 1時 ,其他節點可以分為 m ( m > 0) 個 互不相交的有限集 T1,T2, .... , 其中每個 有限集本身又是一棵樹 ,稱為根節點的子樹(SubTree)

在上圖中 ,節點A 時根節點 ,它包含 T1和 T2兩顆子樹 ,T1={BDGHI} ,T2={CEFJ},每課子樹又是一棵樹 ,在T1子樹中 ,B是根節點 ,在 T2 子樹中 ,C 是根節點

注意 :

​ 當 n > 0 時 ,在非空樹中 ,根節點是唯一的

​ 當 m > 0 時 ,某個節點的子樹時沒有限制的,並且各個子樹肯定是不相交的

3.2 相關概念

1

節點擁有的子樹的數量稱為節點的度(Degree)

度為 0 的節點稱為葉子節點(Leaf)或者終端節點 ,度不為 0 的 節點稱為分支節點或非終端節點 。

除了根節點外 ,分支節點也稱為內部節點 。

樹的度是樹內各個節點中度的最大值 。


2

節點的子樹的根稱為該節點的孩子(Child),相應的該節點稱為孩子節點的雙親(Parent)節點或父節點 ;

例如 :BC 是 A 的子樹 ,BC 是 A 是孩子節點 ,A 是 BC 的雙親節點

父子節點之間的連線是樹的一條邊 ,樹中節點數等於 數的邊數 + 1

在樹中 ,根節點沒有雙親節點 ,其他節點都有並且只有一個父節點 。每個節點可以有多個孩子節點 。

同一個雙親的孩子節點之間 互稱為兄弟(Sibling)

節點的祖先是從根節點到該節點 所經過的分支上的所有節點

以某節點為根的子樹上的任一節點都 稱為該節點的子孫 。


3

節點的層次(Level)是從根節點開始的 ,根為第一層 ,根的孩子 為第二層 ,依次類推 ,注意 :有些人把層次的定義是從0開始的 ,即根為第0 層

如果某節點在第 i 層 ,則其子樹的根就在 i+1 層

雙親節點(父節點)在同一層次上的節點互為堂兄弟 ,例 :DEF 互為堂兄弟

樹中節點的最大層次稱為樹的深度(Depth)或高度,當前樹的高度是 4 ,

在樹中 k+1 個節點通過 k 條邊構成的序列稱為長度 ,為 k 的路徑 。如上圖中 :

{(D,B),(B,A),(A,C),(A,E)} 構成一條連線 D 節點與 E 節點的路徑。該路徑的長度為 4 ;在樹中任意兩個節點都有唯一的路徑 。從根節點開始 ,存在 到其他任意節點的唯一路徑 。

如果將書的節點的各個子樹看作是從左到右的順序 。不能互換 的,則稱該樹為有序樹 。否則稱為無序樹 ,如果不特殊說明 ,一般討論的是有序樹 。

樹中所有節點最大讀書 為 m 的有序樹 稱為 m 叉樹 ; 例 :D節點的度為 3 。則將該有序樹稱為 三叉樹 。

森林(Forest)是 m (m >= 0)顆互不相交的樹的集合 。對樹的每個節點而言 ,其子樹的集合就是森林 ,刪去樹的根就得到一個森林 。反之 ,把森林加上一個樹的根就變成一棵樹 。


3.3 樹的抽象資料型別

ADT Tree{

​ 資料物件 D :D是具有相同性質的資料元素的集合 。

​ 資料物件 R :如果 D 是空則 R 為空;如果 D 不為空 ,D 中存在唯一 一個稱為根的元素 root ;該元素沒有前驅 ,除了根元素外 , D 中每個元素 有且僅有 一個前驅 ;

​ 資料操作 :

​ getSize() :返回元素的個數

​ getRoot() : 返回樹的根元素

​ getParent(x) :返回 x 節點的父節點

​ getFirstChild(x) :返回 x 節點的第一個孩子節點

​ getNextSibling(x) :返回 x 節點的下一個兄弟節點 ,如果 x 是最後還記節點 ,返回null

​ getHeight(x) :返回 以 x 節點為根的樹的高度 。

​ insertChild(x,child) :將節點 child 為根的子樹插入到當前樹中 ,作為 x 節點的孩子節點

​ deleteChild(x,i) :刪除節點 x 的第 i 顆子樹

​ preOrder(x) :先序遍歷 x 為根的樹

​ inOrder(x) :中序遍歷 x 為根的樹

​ postOrder(x) :後序遍歷 x 為根的樹

​ levelOrder(x) :按層次遍歷 x 為根的樹

} ADT Tree

3.4 樹的儲存結構

1、雙親表示法

樹中的節點 ,除了根節點外 ,都有且僅有一個雙親結 點 ,可以在使用陣列儲存樹中的每個節點 。陣列的下標就是陣列的位置指標 ,每個節點再增加一個指向雙親的指標域 ,

節點的結構可以定義為 :

使用該方式

儲存結構為 :

陣列下標 data parent 父節點的儲存下標 firstChild(長子域)
0 A -1 1
1 B 0 3
2 C 0 4
3 D 1 6
4 E 2 9
5 F 2 -1
6 G 3 -1
7 H 3 -1
8 I 3 -1
9 J 4 -1

在雙親表示法儲存結構中 ,可以方便的通過 parent 指標域找到該節點的父節點 ,如果要找到某個節點的孩子節點 ,需要遍歷整個陣列 。

可以在節點中再增加一個長子域 ,指向第一個孩子的指標域 。如果沒有長子域 ,那麼該長子域設定為 -1


#### **2、孩子表示法**

樹中每個節點可能有多顆子樹 ,可以考慮使用多重連結串列 ,每個節點可以有多個指標域 ,每個指標域指向它的子樹的根節點 。把這種方式稱為多重連結串列表示法 。

樹的每個節點的度可能不一樣 ,即每個節點的孩子節點個數不相等 。一般設計以下兩種方案 :

方案一 :

​ 節點中指標域的個數就是數的度(樹中節點最多的孩子樹)

data child1 child2 child3 childn…

節點中孩子節點域的個數就是樹的度

該樹使用孩子表示法 ,可以表示為 :

如果樹種各個節點的度相差很大時 ,很浪費空間 ,有很多節點的指標域是空的 ,這種表示方法適合樹的各個節點度相差很小的情況 。


第二種方案 :每個節點的指標域的個數等於該節點的度 ,再節點中專門定義一個儲存該節點度的域 :

如 節點可以設計為 :

data Degree (度) child1 child2 child3 childn…

上圖的樹還可以表示為 :

這種方法提高了空間的利用率 ,但是各個節點的結構不一樣 ,還要維護節點的度的值 ,會增加時間上的損耗 。


可以定義一個線性表儲存樹中的所有節點的資訊 ,稱為節點表 。每個節點建立一個孩子表 ,孩子表只儲存孩子節點在陣列中的儲存位置 。由於每個節點的孩子節點的個數是不確定的 ,經常使用一個連結串列表示孩子之間的關係 ,這就是孩子表示法 。

如上樹(第二種方案)使用孩子表示法 ,可以表示為 :(節點表包括 下標 、資料) (孩子表包括firstchild)

在上圖這種表示法中 ,需要設計兩種節點 ,一個節點 陣列中表頭節點 ,包括資料域 和 指向第一個孩子節點的指標域 。

​ 如 :

還需要設計一個孩子節點 ,儲存孩子節點在陣列的下標 ,和 指向下個孩子節點的指標 。如下圖 :

​ (指向 當前孩子節點的 兄弟節點 。)

在這種結構中 ,可以方便查詢某個節點的孩子節點 ,也可以方便查詢某個節點的兄弟節點 ,只需要訪問這個節點的孩子連結串列 即可 ,如果需要查詢節點的父節點 ,還需要遍歷整棵樹 :我們可以在節點表中 ,即陣列中的節點增加一個指向父節點的指標 ,如 :

這種表示法稱為雙親孩子表示法 。


3、孩子兄弟表示法

從樹節點的兄弟的角度來確定樹的儲存結構 。

對於任意一棵樹,它的節點的第一個孩子如果存在肯定是唯一的 ,如果節點的右兄弟存在也肯定是唯一的 。可以設定兩個指標分別指向某個節點的第一個孩子和它的右兄弟 。如 :

(資料域 ,第一個孩子節點 ,右兄弟節點)

使用孩子兄弟法表示樹的儲存結構為 :

這種表示法 ,可以方便查詢某個節點的孩子節點和右兄弟節點 。

這種表示法 ,把一顆複雜的樹轉換為一顆二叉樹 。


3.5 二叉樹

3.5.1 二叉樹的特點

二叉樹(Binary Tree)是由 n 個節點組成的 集合 ,該集合要麼是空集合 ,要麼是一個由根節點和兩顆互不相交的二叉樹組成 。

二叉樹的特點 :

​ 1)、每個節點最多有兩顆子樹 ,

​ 2)、左子樹與右子樹是有順序的 ,從左到右

​ 3)、即使樹中的某個節點只有一個子樹 ,那麼也是區分左子樹與右子樹的 。

二叉樹的五種基本形態 :

​ 1、空二叉樹

​ 2、只有一個節點的二叉樹

​ 3、根節點只有左子樹

​ 4、根節點只有右子樹

​ 5、跟節點既有左子樹 又有 右子樹


3.5.2 特殊的二叉樹

1、斜樹

​ 所有節點都只有左子樹的二叉樹稱為左斜樹 。

​ 所有節點都只有右子樹的二叉樹稱為右斜樹 。

2、滿二叉樹

​ 在一顆二叉樹中 ,如果所有分支節點都有左子樹和有右子樹 ,並且所有的葉子節點都在同一層上,這樣的二叉樹稱為滿 二叉樹 。即每層的節點都是滿的 。

滿二叉樹的特點 :

​ 1)、葉子節點只能出現在最下面的一層

​ 2)、非葉子節點的度一定是 2

​ 3)、在同樣深度的二叉樹中 ,滿二叉樹的節點樹是最多的 ,葉子也是最多的 。

3)、完全二叉樹

​ 對一顆具有 n 個節點的二叉樹按層次編號 ,如果編號為 i 的節點與同樣深度的滿二叉樹編號為 i 的節點在二叉樹中的位置完全相同 ,這就是一顆完全二叉樹 。

​ 就是滿二叉樹最下層 從 最右側開始去掉相鄰的若干葉子節點。例 :

滿二叉樹一定是一顆完全二叉樹 ,但完全二叉樹不一定是滿 的

完全二叉樹的特點 :

​ 葉子節點只能出現在最下兩層

​ 最下層的葉子節點集中在左側連續的位置

​ 倒數第二層的葉子節點一定都在右邊連續的位置 。

​ 如果節點的度為 1 ,該節點只有左孩子

​ 同樣節點數的二叉樹 ,完全二叉樹的深度最小 。


3.5.3 二叉樹的性質

1、性質 1

​ 在二叉樹的第 i 層上最多有 2^(i-1) 個節點 (i >= 1)

​ 例 :上圖 >>> 第四層的節點 = 2的 i-1 次方 =2^(4-1) = 8;即第四層有八個節點 。

2、性質 2

​ 深度為 k 的二叉樹 ,最多有 2^k - 1 個節點 (二叉樹總的節點)。

​ 例 :上圖 >>> 如果有一層 ,只有一個根節點 沒有孩子節點 ,2^1 - 1 = 1 (即是根節點 ,也是葉子節點 )

​ 如果有四層 ,2^4 -1 = 15 二叉樹總結點為 15 (包括根節點 )

3、性質 3

​ 對於任意一顆二叉樹 ,葉子節點的數量 n0 ,度為 2 的節點數量 n2 ,則 n0 = n2 + 1

樹中節點數量 = 數的邊(連線) + 1

分支線的數量 = 節點總數 - 1 因為根節點沒有進入的連線

n2 * 2 + n1 = n0(葉子節點) + n1(一個節點) + n2(兩個節點) - 1 (根節點上面沒有連線)

n2 = n0 - 1

4、性質 4

具有 n 個節點的完全二叉樹深度為 floor(log2 n) + 1

滿二叉樹深度為 k ,節點總數量 :2^k - 1 。如果把總結點的數量記為 n ,即 n = 2^k - 1 。則 k = log2(n+1)

深度為 k 的完全二叉樹節點數量 n 一定小於等於同樣深度的滿二叉樹的節點數 。一定大於深度為 k-1 的滿二叉樹節點的數量 。即 :

2^k - 1 >= n > 2^(k-1) - 1 n 就是深度為 k 的完全二叉樹的節點數量

​ n <= 2^k - 1 , 意味著 n < 2^k

​ 2^(k-1) -1 < n , 意味著 2^(k -1) <= n

即 :

​ 2^(k-1) <= n < 2^k

對不等式的兩邊取對數 :得到 >> k-1 <= log2 n < k

因為 k 是深度 ,也是一個整數 , floor(log2 n) + 1

​ floor (xx) 是指 小於等於 xx 的最大整數

5、性質 5

對一個完全二叉樹進行按層次編號

對於任意一個節點 i ,有 :

如果 i==1 ,則節點 i 是二叉樹的根 ,如果 i > 1 , 則該節點的雙親節點是 i / 2 ;

如果 2 * i > n (n 表示節點數量 ) ,則節點 i 沒有左孩子 ,否則左孩子是 2 * i 。

如果 2 * i + 1 > n ,則節點 i 沒有右孩子 ,否則右孩子是 2 * i + 1 .

3.5.4 二叉樹的儲存結構

1、二叉樹的順序儲存

​ 使用一維陣列儲存二叉樹中的 節點 ,節點的儲存位置 (陣列的下標) 可以反映節點之間的邏輯關係。

邏輯關係 :

​ 完全二叉樹的順序儲存 ,對完全二叉樹的各個節點按層次編號

將完全二叉樹儲存到陣列中 ,陣列的下標對應儲存位置 。

如果不是完全二叉樹 ,可以將二叉樹編號 ,把不存在的節點設定為 null 。

如果二叉樹中有很多不存在的節點 ,會造成儲存空間的浪費 。一般情況下 ,順序儲存只用於完全二叉樹 。

**2、鏈式儲存 **

二叉樹的節點最多右兩個孩子 ,可以為節點設計一個數據域 ,一個指向左孩子的指標域 ,和一個指向右孩子的指標域 ,由這樣的節點組成的連結串列稱為二叉連結串列 。

二叉樹節點的結構可以設計為 :

以下二叉樹的二叉連結串列為 :

為了方便找到父節點 ,可以在節點上增加一個指向父節點的指標域 。這種節點組成的連結串列稱為三叉連結串列 。

節點的結構可以設計為 :

上面的二叉樹使用三叉連結串列可以設計為 :

前序遍歷

根節點 > 左子樹 > 右子樹

中序遍歷

左子樹 > 根節點 > 右子樹

後續遍歷

左子樹 > 右子樹 > 根節點

未完待續 .......................... ,