數據結構與算法 :
一.數據結構和算法簡介
數據結構是指數據在計算機存儲空間中的安排方式,而算法時值軟件程序用來操作這些結構中的數據的過程.
二. 數據結構和算法的重要性
幾乎所有的程序都會使用到數據結構和算法,即便是最簡單的程序也不例外.比如,你希望打印出學生的名單,這個程序使用一個數組來存儲學生名單,然后使用一個簡單的
for循環來遍歷數組,最后打印出每個學生的信息.
在這個例子中數組就是一個數據結構,而使用for循環來遍歷數組,則是一個簡單的算法.可見數據結構和算法是構成程序的靈魂所在,而且也有人提出數據結構+算法=程序.
簡單算法
冒泡排序 :
一.核心思想 :
比較兩個元素,如果前一個比后一個大則進行交換.經過對每個元素的比較,最后將最大的元素設置成最后一個元素.重復該操作,最后形成從小到大的排序.
選擇排序
一.核心思想 :
掃描所有的元素,得到最小的元素,並將最小的元素與左邊第一個元素進行交換.再次掃描除第一位置的所有元素,得到最小的元素,與左邊第二個元素進行交換.依次類推.
例子 :
//選擇排序
public void selectSort() {
int main = 0;
long tmp = 0L;
for (int i = 0; i < elems-1; i++) {
if (arr[j] < arr[min]) {
min = j;
}
}
tmp = arr[i];
arr[i] = arr[min];
arr[min] = tmp;
}
插入排序 :
一.核心思想 :
抽出一個元素,在其前面的元素中找到適當的位置進行插入.
例子 :
//插入排序
public void insertSort() {
long select = 0L;
for (int i = 11; i < elems; i++) {
select = arr[i];
int j = 0;
for (j = i; j > 0 && arr[j - 1] >= select; j--) {
arr[j] = arr[j - 1];
}
arr[j] = select;
}
}
棧 :
核心思想 : 棧只允許訪問一個數據項,也就是最后插入的數據項.只有移除了找個數據項才能夠訪問倒數第二個插入的數據項.
例子 :
//模擬棧
package com.example.demo.testDemo;
public class MyStack {
private int maxSize;
private long[] arr;
private int top;
//構造方法
public MyStack(int size) {
maxSize = size;
arr = new long[maxSize];
top = -1;
}
// 壓入數據
public void push(long value) {
arr[++top] = value;
}
// 彈出數據
public long pop() {
return arr[top--];
}
//訪問棧頂元素
public long peek() {
return arr[top];
}
// 棧是否為空
public boolean isEmpty() {
return (top == -1);
}
// 棧是否滿了
public boolean isFull() {
return (top == maxSize - 1);
}
}
package com.example.demo.testDemo;
public class TestStack {
public static void main(String[] args) {
MyStack ms = new MyStack(10);
ms.push(40);
ms.push(30);
ms.push(20);
ms.push(10);
ms.push(-10);
ms.push(-20);
while (!ms.isEmpty()) {
System.out.println(ms.pop());
}
}
}
隊列
核心思想 : 隊列是一種數據結構,類似於棧,不同的是在隊列中第一個插入的數據項會最先被移除.也就是先進先出.
例子 : 循環隊列
package com.example.demo.testDemo;
public class Queue {
// 數組
private long[] arr;
// 最大空間
private int maxSize;
// 有效元素大小
private int elems;
// 隊頭
private int font;
// 隊尾
private int end;
public Queue(int maxSize) {
this.maxSize = maxSize;
arr = new long[maxSize];
elems = 0;
font = 0;
end = -1;
}
// 插入數據
public void insert(long value) {
// 循環插入
if (end == maxSize - 1) {
end = -1;
}
arr[++end] = value;
elems++;
}
// 移除數據
public long remove() {
long tmp = arr[font++];
if (font == maxSize) {
font = 0;
}
elems--;
return tmp;
}
// 是否為空
public boolean isEmpty() {
return (elems == 0);
}
// 是否滿了
public boolean isFull() {
return (elems == maxSize);
}
// 返回有效元素大小
public int size() {
return elems;
}
}
優先級隊列
核心思想 : 在優先級隊列中,數據項按關鍵字的值有序,這樣關鍵字最小的數據項(或者最大)總是在隊頭.數據項插入時會按照順序插入到合適的位置以確保隊列的順序.
例子 :
package com.example.demo.testDemo;
public class PriorityQueue {
// 數組
private long[] arr;
// 最大空間
private int maxSize;
// 有效元素大小
private int elems;
public PriorityQueue(int maxSize) {
this.maxSize = maxSize;
arr = new long[maxSize];
elems = 0;
}
// 插入數據
public void insert(long value) {
int i;
for (i = 0; i < elems; i++) {
if (arr[i] < value) {
break;
}
}
for (int j = elems; j > i; j--) {
arr[j] = arr[j - 1];
}
arr[i] = value;
elems++;
}
// 移除數據
public long remove() {
long value = arr[elems - 1];
elems--;
return value;
}
// 是否為空
public boolean isEmpty() {
return (elems == 0);
}
// 是否清了
public boolean isFull() {
return (elems == maxSize);
}
//返回有效元素大小
public int size() {
return elems;
}
}
package com.example.demo.testDemo;
public class TestPQ {
public static void main(String[] args) {
PriorityQueue priorityQueue = new PriorityQueue(10);
priorityQueue.insert(30);
priorityQueue.insert(2);
priorityQueue.insert(45);
priorityQueue.insert(1);
priorityQueue.insert(15);
while (!priorityQueue.isEmpty()) {
long value = priorityQueue.remove();
System.out.println(value);
}
}
}
鏈接點 :
核心思想 : 鏈接點中包含一個數據域還一個指針域,其中數據域用來包裝數據,而指針域用來指向下一個鏈接點.
例子 :
package com.example.demo.testDemo;
public class Link {
// 數據域
private long data;
// 指針域
private Link next;
public Link(long data) {
this.data = data;
}
public long getData() {
return data;
}
public void setData(long data) {
this.data = data;
}
public Link getNext() {
return next;
}
public void setNext(Link next) {
this.next = next;
}
}
package com.example.demo.testDemo;
public class TestLink {
public static void main(String[] args) {
Link l1 = new Link(10);
Link l2 = new Link(45);
Link l3 = new Link(360);
Link l4 = new Link(1);
l1.setNext(l2);
l2.setNext(l3);
l3.setNext(l4);
System.out.println(l1.getData());
System.out.println(l1.getNext().getData());
System.out.println(l1.getNext().getNext().getData());
System.out.println(l1.getNext().getNext().getNext().getData());
}
}
鏈表 :
核心思想 : 鏈表中只包含 一個數據項,既對第一個鏈接點的引用.
例子 :
package com.example.demo.testDemo;
public class LinkList {
private Link first;
public void insert(long value) {
Link link = new Link(value);
if (first == null) {
first = link;
} else {
link.setNext(first);
first = link;
}
}
public void displayAll() {
Link current = first;
while (current != null) {
System.out.println(current.getData());
current = current.getNext();
}
}
//查找指定結點
public Link find(Long key) {
Link current = first;
while (current.getData() != key) {
if (current.getNext() == null) {
return null;
}
current = current.getNext();
}
return current;
}
// 插入結點到指定位置
public void insert(long value, int pos) {
if (pos == 0) {
insert(value);
} else {
Link current = first;
for (int i = 0; i < pos - 1; i++) {
current = current.getNext();
}
Link link = new Link(value);
link.setNext(current.getNext());
current.setNext(link);
}
}
// 刪除指定結點
public void delete(long key) {
Link current = first;
Link ago = first;
while (current.getData() == key) {
if (current.getNext() == null) {
return;
} else {
ago = current;
current = current.getNext();
}
}
if (current == first) {
first = first.getNext();
} else {
ago.setNext(current.getNext());
}
}
}
package com.example.demo.testDemo;
public class TestLinkList {
public static void main(String[] args) {
LinkList linkList = new LinkList();
linkList.insert(40);
linkList.insert(12);
linkList.insert(23);
linkList.insert(10);
linkList.displayAll();
System.out.println("找到結點,數據為 : " + linkList.find(10L).getData());
}
}
三角數字
核心 : 數列中的第一項為1,第n項由n-1項加n得到.
例子 :
public static int SanJiao(int n) {
int total = 0;
while(n > 0) {
total = total + n;
n--;
}
return total;
}
public static int SanJiaoDiGui(int n) {
if (n == 1) {
return 1;
} else {
return n + SanJiaoDiGui(--n);
}
}
Fibonacci數列 : 中的第1,2項為1,第n項由n-1項加n-2項得到.
public static int fibonacciMeth(int n) {
if (n == 1 || n == 2) {
return 1;
} else {
return fibonacciMeth(n-1) + fibonacciMeth(n-2);
}
}
數據結構(data structure)是指相互之間存在一種或多種特定關系的數據元素的集合.
是組織並存儲數據以便能夠有效使用的一種專門格式,它用來反映一個數據的內部構成,既一個數據由那些成分數據構成,以什么方式構成,呈什么結構.
由於信息可以存在於邏輯思維領域,也可以存在於計算機世界,因此作為信息載體的數據同樣存在於兩個世界中.
表示一組數據元素及其相互關系的數據結構同樣也有兩種不同的表現形式,一種是數據結構的邏輯層面,既數據的邏輯結構;一種是存在於計算機世界的物理層面,既數據的
存儲結構.
數據結構 = 邏輯結構 + 存儲結構 + (在存儲結構上的)運算/操作;
數據的邏輯結構
數據的邏輯結構指數據元素之間的邏輯關系(和實現無關).
分類1 : 線性結構和非線性結構
線性結構 : 有且只有一個開始結點和一個終端結點,並且所有節點都最多只有一個直接前驅和一個直接后繼.
線性表就是一個典型的線程結構,它有四個基本特征 :
1.集合中必存在唯一的一個"第一個元素";
2.集合中必存在唯一的一個"最后的元素";
3.除最后元素之外,其他數據元素均有唯一的"直接后繼";
4.除第一元素之外,其他數據元素均有唯一的"直接前驅".
生活案例 : 冰糖葫蘆,排隊上地鐵
相對應線性結構,非線性結構的邏輯特征是一個結點元素可能對應多個直接前驅和多個直接后繼.
常見的非線性結構有 : 樹(二叉樹),圖(網等)
樹 生活案例 : 單位組織架構,族譜.技術案例 : 文件系統.
分類2 : 線性結構,樹狀結構,網絡結構
邏輯結構有三種基本類型 : 線性結構,樹狀結構和網絡結構.
表和樹是最常用的兩種高效數據結構,許多高效的算法能夠用這兩個數據結構來設計實現.
線性結構 : 數據結構中線性結構值得是數據元素之間存在着"一對一"的線性關系的數據結構.
樹狀結構 : 除了一個數據元素(元素01)以外每個數據元素有且僅有一個直接前驅元素,但是可以有多個直接后續元素.
特點是數據元素之間是一對多的聯系.
網絡結構 : 每個數據元素可以有多個直接前驅元素,也可以有多個直接后續元素.特點是數據元素之間是多對多的聯系.
數據的存儲結構
數據的存儲結構主要包括數據元素本身的存儲以及數據元素之間關系表示,是數據的邏輯結構在計算機中的表示.常見的存儲結構有順序存儲,鏈式存儲,索引存儲,以及散列存儲.
順序存儲結構 : 把邏輯上相鄰的節點存儲在物理位置上相鄰的存儲單元中,結點之間的邏輯關系由存儲單元的鄰接關系來體現.
由此得到的存儲結構為順序存儲結構,通常順序存儲結構是借助於計算機程序設計語言(例如C/C++)的數組來描述的.
(數據元素的存儲對應於一塊連續的存儲空間,數據元素之間的前驅和后續關系通過數據元素,在存儲器中的相對位置來反映)
例如 :
0 1 ... i-1 i n-1 ...MAXSIZE-1
data a1 a2 ... ai-1 ai ai+1 ... an ...
鏈式存儲結構 :
數據元素的存儲對應的是不連續的存儲空間,每個存儲節點對應一個需要存儲的數據元素.
每個結點是由數據域和指針域組成.元素之間的邏輯關系通過存儲節點之間的鏈接關系反映出來.
邏輯上相鄰的節點物理上不必相鄰.
例如 : 數據 指針
head -> a1 -> a2 ->----> an null
頭指針 表
索引存儲結構 : 除建立存儲結點信息外,還建立附加的索引表來標識結點的地址.
例如 : 圖書,字典的目錄
散列存儲結構 : 根據結點的關鍵字直接計算出該結點的存儲地址HashSet,HashMap;
一種神奇的結構,添加,查詢速度快.
線性表(linear list)
線性表是n個類型相同數據元素的有限序列,通常記作(a0,a1,a2,a3,a4,...,ai,ai+1);
1.相同數據類型
在線性表的定義中,我們看到從a0到a(n-1)的n個數據元素是具有相同屬性的元素.
比如說可以都是數字,例如(23,14,66,5,99);
也可以是字符,例如(A,B,C,...Z);
當然也可以是具有更復雜結構的數據元素,例如學生,商品,裝備.
相同數據類型意味着在內存中存儲時,每個元素會占用相同的內存空間,便於后續的查詢定位.
2.序列(順序性)
在線性表的相鄰數據元素之間存在着序偶關系,
既ai-1是ai的直接前驅,則ai是ai-1的直接后續,
同時ai又是ai+1的直接前驅,ai+1是ai的直接后續.
唯一沒有直接前驅的元素a0一端稱為表頭,
唯一沒有后續的元素an-1一端稱為表尾部.
除了表頭和表尾元素外,任何一個元素都有且僅有一個直接前驅和直接后繼.
3.有限
線性表中數據元素的個數n定義為線性表的長度,n是一個有限值.
當n=0時線性表為空表.
在非空的線性表中每個數據元素在線性表中都有唯一確定的序號,例如a0的序號是0,ai的序號是i.
在一個具有n > 0個數據元素的線性表中,數據元素序號的范圍是[0,n-1].
生活案例 : 冰糖葫蘆,多個學生分數,多個學生數據.
順序表 ---順序存儲結構
0 1 ... i-1 i n-1 ...MAXSIZE-1
data a1 a2 ... ai-1 ai ai+1 ... an ...
last
特點 : 在內存中分配連續的空間,只存儲數據,不需要存儲地址信息.位置就隱含着地址.
優點 :
1.節省存儲空間,因為分配給數據的存儲單元全用存放結點的數據(不考慮C/C++語言中數組需指定大小的情況),結點之間邏輯關系沒有占用額外的存儲空間.
2.索引查找效率高,既每一個結點對應一個序號,由該序號可以直接計算出來結點的存儲地址.
假設線性表的每個數據元素需占用K個存儲單元,並以元素所占的第一個存儲單元的地址作為數據元素的存儲地址.
則線性表中序號為i的數據元素的存儲地址LOC(ai)與序號為i+1的數據元素的存儲地址LOC(ai+1)之間的關系為
LOC(ai+1) = LOC(ai) + K
通常來說,線性表的i號元素ai的存儲地址為
LOC(ai) = LOC(a0) + i*K
其中LOC(a0)為0號元素a0的存儲地址,通常稱為線性表的起始地址.
缺點 :
1.插入和刪除操作需要移動元素,效率較低.
2.必須提前分配固定數量的空間,如果存儲元素少,可能導致空閑浪費.
3.按照內容查詢效率低,因為需要逐個比較判斷.
鏈表 ---鏈式存儲結構
數據 指針
head -> a1 -> a2 ->----> an null
頭指針 表
特點 : 數據元素的存儲對應的是不連續的存儲空間,每個存儲結點對應一個需要存儲的數據元素.
每個結點是由數據域和指針域組成.元素之間的邏輯關系通過存儲節點之間的鏈接關系反映出來.
邏輯上相鄰的節點物理上不必相鄰.
缺點 :
1.比順序存儲結構的存儲密度小(每個節點都由數據域和指針域組成,所以相同空間內假設全存滿的話順序比鏈式存儲更多).
2.查找結點時鏈式存儲要比順序存儲慢(每個節點地址不連續,無規律,導致按照索引查詢效率低下).
優點 :
1.插入,刪除靈活(不必移動節點,只要改變節點中的指針,但是需要先定位到元素上).
2.有元素才會分配結點空間,不會有閑置的結點.
在使用單鏈表實現線性表的時候,為了使程序更加簡潔,我們通常在單鏈表的最前面添加一個啞元結點,也稱為頭結點.
在頭結點中不存儲任何實質的數據對象,其next域指向線性表中0號元素所在的結點,可以對空表,非空表的情況以及首元結點進行統一處理,編程更方便,常用頭結點.
一個帶頭結點的單鏈表實現線性表的結構圖如圖所示.
雙向鏈表
單鏈表一個優點是結構簡單,但是它也有一個缺點,既在單鏈表中只能通過一個結點的引用訪問其后續結點,而無法直接訪問其前驅結點,要在單鏈表中找到某個結點的前驅
結點,必須從鏈表的首結點出發依次向后尋找,但是需要O(n)時間.
為此我們可以擴展單鏈表的結點結構,使得通過一個結點的引用,不但能夠 訪問其后續結點,也可以方便的訪問其前驅結點.擴展單鏈表結點結構的方法是,在單鏈表結點
結構中新增加一個域,該域用於指向結點的直接前驅結點.擴展后的結點結構是構成雙向鏈表的結點結構,如圖所示.
前驅指針域 數據域 后續指針域
pre data next
雙向鏈表是通過上述定義的結點使用pre以及next域依次串聯在一起而形成的.一個雙向鏈表的結構如圖所示.
head tail
^ a0 -> a1 -> a2 -> a3^
<- <- <-
在雙向鏈表中同樣需要完成數據元素的查找,插入,刪除等操作.在雙向鏈表中進行查找與在單鏈表中類似,只不過在雙向鏈表中查找操作可以從鏈表的首結點開始,也可以
從尾結點開始,但是需要的時間和在單鏈表中一樣.
循環鏈表
在一個循環鏈表中,首節點和末節點被連接在一起.這種方式在單向和雙向鏈表中皆可實現.要轉換一個循環鏈表,你開始於任意一個節點然后沿着列表的任一方向直到返回
開始的節點.循環鏈表可以被視為"無頭無尾".
循環鏈表中第一個節點之前就是最后一個節點,反之亦然.循環鏈表的無邊界使得在這樣的鏈表上設計算法會比普通鏈表更加容易.對於新加入的節點應該是在第一個節點
之前還是最后一個節點 之后可以根據實際要求靈活處理,區別不大.
單向列表的循環帶頭結點的非空鏈表.
| - - - - - - - -- - -|
->^ ->a1 -> a2 ->... ->an -|
單向鏈表的循環帶頭結點的空鏈表
雙向鏈表的循環帶頭結點的非空鏈表
棧和隊列都是操作受限的線性表.
棧的定義
棧(stack)又稱堆棧,它是運算受限的線性表.
其限制是僅允許在表的一端進行插入和刪除操作,不允許在其他任何位置進行插入,查找,刪除等操作.
表中進行插入,刪除操作的一端稱為棧頂(top),棧頂保存的元素稱為棧頂元素.
相對的,表的另一端稱為棧底(bottom)
當棧中沒有數據元素時稱為空棧;向一個棧插入元素又稱為進棧或入棧;從一個棧中刪除元素又稱為出棧或退棧.由於棧的插入和刪除操作僅在棧頂進行,后進棧的元素必定
先出棧,所以又把堆棧稱為后進先出表(LIFO) <- 棧頂
D <-棧頂
C C
<-棧頂 B B
<-棧頂/底 A <-棧底 A <- 棧底 A <-棧底
空棧 A入棧 BCD入棧 D出棧
生活案例 : 摞盤子和取盤子,一摞書,酒被塔(各層之間可以簡單理解為棧,每層內部不是棧)
技術案例 : Java的棧內存.
棧可以保存底層方法的信息.
隊列定義
隊列(queue)簡稱隊,它同堆棧一樣,也是一種運算受限的線性表,其限制是僅允許在表的一端進行插入,而在表的另一端進行刪除.在隊列中把插入數據元素的一端稱為隊尾
(rear),刪除數據元素的一端稱為隊首(front).向對尾插入元素稱為進隊或入隊,新元素入隊后成新的隊尾元素;從隊列中刪除元素稱為離隊或出隊,元素出隊后,其后續元素
成為新的隊首元素.由於隊列的插入和刪除操作分別在隊尾和隊首進行,每個元素必然按照進入的次序離隊,也就是說先進隊的元素必然先離隊,所以稱隊列為先進先出表(FIFO)
雙端隊列deque 通常為deck
所謂雙端隊列是指兩端都可以進行進隊和出隊操作的隊列, 如下圖所示,將隊列的兩端分別稱為前端和后端,兩端都可以入隊和出隊.其元素的邏輯結構任是線性結構.
前端進 后端進
前端 <--> 后端
前端出 <--> 后端出
在雙端隊列進隊時 : 前端進的元素排列在隊列中后端進的元素的前面,后端進的元素排列在隊列中前端進的元素的后面.在雙端隊列出隊時,無論前端出還是后端出,先出
的元素排列在后出的元素的前面.
輸出受限的雙端隊列,既一個端點運行插入和刪除,另一個端點只允許插入的雙端隊列.
前端進
<-> 前端 后端 <- 后端進
前端出
輸入受限的雙端隊列,既一個端點運行插入和刪除,另一個端點只允許刪除的雙端隊列.
前端出 后端進
前端 <- 后端 <->
后端出
雙端隊列既可以用來隊列操作,也可以用來實現棧操作(只操作一端就是棧了)
樹
樹是由一個集合以及在該集合上定義的一種關系構成的.集合中的元素稱為樹的結點,所定義的關系稱為父子關系.
父子關系在樹的結點之間建立了一個層次結構.樹的結點包含一個數據元素及若干指向其子樹的若干分之.在這種層次結構中有一個結點具有特殊的地位,這個結點稱為該
樹的根結點,或簡稱為樹根.
我們可以形式地給出樹的遞歸定義如下 :
樹(tree)是n(n>=0)個結點的有限集.它
1>或者是一顆空樹(n=0),空樹不包含任何結點.
2>或者是一顆非空樹(n>0),此時有且僅有一個特定的稱為根(root)的結點;
當n > 1時,其余結點可分為m(m > 0)個互不相交的有限集T1,T2,,,,,,Tm.其中每一個本身又是一棵樹,並且稱為根的子樹(sub tree).
(a) A
A / | \
(b) B C D
/ \ | /|\
E F G H I J
/\ |
K L M
例如圖 (a)是一顆空樹,(b)是只有一個根節點的樹,(c)是一顆有10個節點的樹,其中A是根,其余的節點分成3個不相交的集合 : T1 = {B,E,F}, T2 = {C,G}, T3 = {D,H,J},
每個集合都構成一棵樹,且都是根A的子樹.
生活案例 : 樹 : 單位組織架構,族譜.
結點的度與樹的度
結點擁有的子樹的數目稱為結點的度(Degree).
度為0的結點稱為葉子(leaf)或終端節點.度不為0的結點稱為非終端結點或分支結點.除根之外的分支結點也稱為內部結點.樹內各結點的度最大值稱為樹的度.
上圖中A : 此結點度為3; K : 此結點度為0; H : 此結點度為1; 此樹的度為3
父親,兒子,兄弟
父親(parent) : 一個結點的直接前驅結點.
兒子(child) : 一個結點的直接后繼結點.
兄弟(sibling) : 同一個父親結點的其他結點.
結點A是結點B,C,D的父親,結點B,C,D是結點的A的孩子.
由於結點H,I,J有同一個父結點D,因此他們互為兄弟.
祖先,子孫,堂兄弟
將父子關系進行擴展,就可以得到祖先,子孫,堂兄弟等關系.
結點的祖先是從根到該結點路徑上的所有結點.
以某結點為根的樹中的任一結點都稱為該結點的子孫.
父親在同一層次的結點互為堂兄弟.
二叉樹 :
每個結點的度均不超過2的有序樹,稱為二叉樹(binary tree).(樹中每個節點最多只能有兩個子節點)
與樹的遞歸定義類似,二叉樹的遞歸定義如下 :
二叉樹或者是一顆空樹,或者是一顆由一個根結點和兩顆互不相交的分別稱為根的左子樹和右子樹的子樹所組成的非空樹.
例如 :
E
/\
A G
\ \
C F
/\
B D
圖1
由以上定義可以看出 :
二叉樹中每個結點的孩子數只能是0,1或2個,並且每個孩子都有左右之分.位於左邊的孩子稱為左孩子,位於右邊的孩子稱為右孩子;以左孩子為根的子樹稱為左子樹,
以右孩子為根的子樹稱為右子樹.
插入節點
核心思想 :
1.如果不存在節點,則直接插入.
2.從根開始查找一個相應的節點,既新節點的父節點,當父節點找到后,根據新節點的值來確定新節點是連接到左子節點還是右子節點.
查找節點
核心思想 :
1.從根開始查找,如果查找節點值比父節點值要小,則查找其左子樹,否則查找其右子樹,直到查到為止,如果不存在節點,則返回null.
修改節點
核心思想 :
1.首先查找節點,找到相應節點后,再進行修改.
遍歷二叉樹
核心思想 : 分為三種方法,一種是先序遍歷,一種是中序遍歷,一種是后序遍歷.
先序遍歷二叉樹
核心思想 : 訪問節點.遍歷節點的左子樹,遍歷節點的右子樹.
中序遍歷二叉樹
核心思想 : 中序遍歷節點的左子樹,訪問節點,中序遍歷節點的右子樹.
例子 :
package com.example.demo.TreeTest;
public class Node {
// 關鍵字
private int keyData;
// 其他數據
private int otherData;
// 左子結點
private Node leftNode;
// 右子節點
private Node rightNode;
public Node(int keyData, int otherData) {
this.keyData = keyData;
this.otherData = otherData;
}
public int getKeyData() {
return keyData;
}
public void setKeyData(int keyData) {
this.keyData = keyData;
}
public int getOtherData() {
return otherData;
}
public void setOtherData(int otherData) {
this.otherData = otherData;
}
public Node getLeftNode() {
return leftNode;
}
public void setLeftNode(Node leftNode) {
this.leftNode = leftNode;
}
public Node getRightNode() {
return rightNode;
}
public void setRightNode(Node rightNode) {
this.rightNode = rightNode;
}
// 顯示方法
public void disPlay() {
System.out.println(keyData + "," + otherData);
}
}
package com.example.demo.TreeTest;
public class Tree {
// 根
private Node root;
// 插入方法
public void insert(int keyData, int otherData) {
Node node = new Node(keyData, otherData);
// 如果說沒有節點
if (root == null) {
root = node;
} else {
Node current = root;
Node parent;
while (true) {
parent = current;
if (keyData < current.getKeyData()) {
current = current.getLeftNode();
if (current == null) {
parent.setLeftNode(node);
return;
}
} else {
current = current.getRightNode();
if (current == null) {
parent.setRightNode(node);
return;
}
}
}
}
}
// 查找方法
public Node find(int keyData) {
Node current = root;
while (current.getKeyData() != keyData) {
if (keyData < current.getKeyData()) {
current = current.getLeftNode();
} else {
current = current.getRightNode();
}
if (current == null) {
return null;
}
}
return current;
}
// 刪除方法
public void delete(int keyData) {
}
// 修改方法
public void change(int keyData, int newOtherData) {
Node node = find(keyData);
node.setOtherData(newOtherData);
}
// 先序遍歷方法
public void preOrder(Node node) {
if (node != null) {
node.disPlay();
preOrder(node.getLeftNode());
preOrder(node.getRightNode());
}
}
}
package com.example.demo.TreeTest;
public class TestTree {
public static void main(String[] args) {
Tree tree = new Tree();
tree.insert(1, 1);
tree.insert(2, 2);
tree.insert(3, 3);
Node findnODE = tree.find(3);
findnODE.disPlay();
}
}
滿二叉樹 :
高度為k並且有2(k+1)-1個結點的二叉樹.
在滿二叉樹中,每層結點都達到最大數,既每層結點都是滿的,因此稱為滿二叉樹.
完全二叉樹 :
若在一顆滿二叉樹中,在最下層從最右側起去掉相鄰的若干葉子結點,得到的二叉樹即為完全二叉樹.
1 1
/\ /\
2 3 2 3
/\ /\ /\ /\
4 5 6 7 4 5 6 7
/\ /\ /\ /\ /\ /\ /
8 9 10 11 12 13 14 15 8 9 10 11 12
(a)滿二叉樹 (b)完全二叉樹
滿二叉樹必為完全二叉樹,而安全二叉樹不一定是滿二叉樹.
圖1
二叉樹的存儲結構 :
二叉樹的存儲結構有兩種 : 順序存儲結構和鏈式存儲結構.
鏈式存儲結構
設計不同的結點結構可構成不同的鏈式存儲結構.
在二叉樹中每個結點都有兩個孩子,則可以設計每個結點至少包括3個域 : 數據域,左孩子和右孩子域.
數據域存放數據元素,左孩子域存放指向左孩子結點的指針,右孩子域存放指向右孩子結點的指針.如圖(a)所示.
利用此結點結構得到的二叉樹存儲結構稱為二叉鏈表.
為了方便找到父結點,可以在上述結點結構中增加一個指針域,指向結點的父結點.如圖(b)所示.
采用此結點結構得到的二叉樹存儲結構稱為三叉鏈表.
1
/\
4 2
\ /\
5 3 6
\
7
遍歷(Traverse) :
就是按照某種次序訪問樹中的所有結點,且每個結點恰好訪問一次.也就是說,按照被訪問的次序,可以得到由樹中所有結點排成的一個序列.樹的遍歷也可以看成是人為的將
非線性結構線性化.這里的"訪問"是廣義的,可以 是對結點作各種處理,例如輸出結點信息,更新結點信息等.在我們的實現中,並不真正的"訪問"這些結點,而是得到一個結點
的線性序列,以線性表的形式輸出.
將整個二叉樹看做三部分 : 根,左子樹,右子樹,如果規定先遍歷左子樹,再遍歷右子樹.
那么根據根的遍歷順序就有三種遍歷方式 :
先序/根遍歷DLR : 根,左子樹,右子樹
中序/根遍歷LDR : 左子樹,根,右子樹
后根/序遍歷LRD : 左子樹,右子樹,根
注意 : 由於樹的遞歸定義,其實對三種遍歷的概念其實也是一個遞歸的描述過程.
先序遍歷DLR : 1 4 5 2 3 6 7
中序遍歷LDR : 4 5 1 3 2 6 7
后序遍歷LRD : 5 4 3 7 6 2 1
面試題 : 已知一顆二叉樹的后序遍歷的序列為5 4 3 7 6 2 1, 中序遍歷的序列為 4 5 1 3 2 6 7,則其先序遍歷的序列是什么?
(首先明白一點,只給先序和后序無法求出中序的,中序是必須給出的)
先序遍歷為 : 1,4,5,2,3,6,7
1
/\
4 2
\ /\
5 3 6
\
7
例如 :
E
/\
A G
\ \
C F
/\
B D
先序遍歷(根,左子樹,右子樹) 中序遍歷(左子樹,根,右子樹) 后序遍歷(左子樹,右子樹,根) 層次遍歷(借助隊列來實現)
E,A,C,B,D,G,F A,B,C,D,E,G,F B,D,C,A,F,G,E E,A,G,C,F,B,D
二叉查找/搜索/排序樹 BST(binary search/sort tree)
或者是一顆空樹 :
或者是具有下列性質的二叉樹 :
(1) 若它的左子樹不空,則左子樹上所有結點的值均小於它的根節點的值;
(2) 若它的右子樹上所有結點的值均大於它的根節點的值;
(3) 它的左,右子樹葉分別為二叉樹.
例子 :
7 8
/\ /\
3 17 3 10
/\ /\ /\ \
2 4 13 20 1 6 14
/\ / /\ /
9 15 18 4 7 13
3 A
/\ \
2 8 B
/\ / \
2 3 6 C
/ \
5 D
/ \
4 E
注意 : 對二叉查找樹進行中序遍歷,得到有序集合.
平衡二叉樹(Self-balancing binary search tree) 自平衡二叉查找樹,又被稱為AVL樹 (有別於AVL算法)
它是一顆空樹或它的左右兩個子樹的高度差(平衡因子)的絕對值不超過1,並且左右兩個子樹都是一顆平衡二叉樹,同時,平衡二叉樹必定是二叉搜索樹,反之則不一定
平衡因子(平衡度) : 結點的平衡因子是 結點的左子樹的高度減去右子樹的高度.(或反之定義)
平衡二叉樹 : 每個結點的平衡因子都為1,-1,0的二叉排序樹.或者說每個結點的左右子樹的高度最多差 1的二叉排序樹.
平衡二叉樹的目的是為了減少二叉查找樹層次,提供查找速度.
平衡二叉樹的常用實現方法有AVL,紅黑樹,替罪羊樹 ,Treap,伸展樹等.
例子 :
20
/\
10 30
/\ /\
7 14 25 40
/ \
4 8
平衡二叉樹
紅黑樹
R-B Tree,全稱是Red-Black Tree,又稱為"紅黑樹",它是一種平衡二叉樹.紅黑樹的每個節點上都有存儲位表示節點的顏色,可以是紅(Red)或黑(Black).
紅黑樹的特性 :
(1) 每個節點或者是黑色,或者是紅色.
(2) 根節點時黑色.
(3) 每個葉子節點(NIL)是黑色.[注意 : 這里葉子節點,是指為空(NIL或NULL)的葉子節點!]
(4) 如果一個節點是紅色的,則它的子節點必須是黑色的.
(5) 從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點.
注意 :
(01) 特性(3)中的葉子節點,是只為空(NIL或null)的節點.
(02) 特性(5),確保沒有一條路徑會比其他路徑長出兩倍.因而,紅黑樹是相對是接近平衡的二叉樹.
例如 :
80
/\
40 120
/\ /\
20 60 100 140
/\ /\ /\ /\
10 NIL 50 NIL 90 NIL 10 30
/\ /\ /\ /\
NIL NIL NIL NIL NIL NIL NIL NIL
紅黑樹
紅黑樹的應用比較廣泛,主要是用它來存儲有序的數據,它的時間復雜度是O(logN),效率非常之高.
它雖然是復雜的,但它的最壞情況運行時間也是非常良好的,並且在實踐中是高效的 : 它可以在O(log n)時間內做查找,插入和刪除,這里的n是樹中元素的數目.
例如 : java集合中的TreeSet和TreeMap,C++STL中的set,map,以及linux虛擬內存的管理,都是通過紅黑樹去實現的.
圖的基本概念 : 多對多關系
圖(graph)是一種網站數據結構,圖是由非空的頂點集合和一個 描述頂點之間關系的集合組成.
其形式化的定義如下 :
Graph = (V,E)
V = {x|x<-某個數據對象}
E = {<u,v>|P(u,v)^(u,v<-V)}
V是具有相同特性的數據元素的集合,V中的數據元素通常稱為頂點(Vertex),
E是兩個頂點之間關系的集合.P(u,v)表示u和v之間有特定的關聯屬性.
若<u,v><-E,則<u,v>表示從頂點v的一條弧,並稱u為弧尾或起始點,稱v為弧頭或終止點,此時圖中的頂點之間的連線是有方向的,這樣的圖稱為有向圖(directedgraph).
若<u,v><-E,則必有<u,v><-E,既關系E是對稱的,此時可以使用一個無序對(u,v)來代替兩個有序對象,它表示頂點u和頂點v之間的一條邊,此時圖中頂點之間的連線是沒有
方向的,這種圖稱為無向圖(undirected graph).
在無向圖和 有向圖中V中的元素都稱為頂點,而頂點之間的關系卻有不同的稱謂,既弧或邊,為避免麻煩,在不影響理解的前提下,我們統一的將它們稱為邊(edge).
並且我們還約定頂點集與邊集都是有限的,並記頂點與邊的數量為|V|和|E|.
a -- d a --> d
|\ | /|\ |
| c | | \|/
b \c b c
無向圖實際上也是有向圖,是雙向圖.
加權圖 :
在實際應用中,圖不但需要表示元素之間是否存在某種關系,而且圖的邊往往與具有一定實際意義的數有關,既每條邊都有與它相關的實數,稱為權.
這些權值可以表示從一個訂單到另一個頂點的距離或消耗等信息,在本章中假設邊的權均為正數.這種邊上具有權值的圖稱為帶權圖(weighted graph)
圖的遍歷 :
圖的遍歷是和樹的遍歷類似,我們希望從圖中某一頂點出發訪遍圖中其余頂點,且使得每一個頂點僅被訪問一次,這一過程就叫做圖的遍歷.
深度優先遍歷 :
也有稱為深度優先搜索,簡稱為DFS.
它從圖中某個頂點V出發,訪問此頂點,然后從V的未被訪問的鄰接點出發深度優先遍歷圖,直至圖中所有和V有路徑相遇的頂點都被訪問到.
例子 :
publi class Graph {
private int vertexSize; //頂點數量
private int[] vertexs; //頂點數組
private int[][] matrix;
private static final int MAX_WEIGHT = 1000;
private boolean[]isVisited;
public Graph(int vertextSize) {
this.vertexSize = vertextSize;
matrix = new int[vertextSize][vertextSize];
vertexs = new int[vertextSize];
for (int i = 0; i < vertextSize; i++) {
vertexs[i] = i;
}
isVisited = new boolean[vertexSize];
}
/**
獲取某個頂點的第一個鄰接點
*/
public int getFirsNeighbor(int index) {
for (int j = 0; i < vertexSize; j++) {
if (matrix[index][j] > 0 && matrix[index][j] < MAX_WEIGHT) {
return j;
}
}
return -1;
}
/**
根據前一個鄰接點的下標來取得下一個鄰接點
v1 : 表示要照的頂點
v2 : 表示該頂點相對於哪個鄰接點去獲取下一個鄰接點
*/
public int getNextNeighbor(int v, int index) {
for (int j = index + 1; j < vertexSize; j++) {
if (matrix[v][j] > 0 && matrix[v][j] < MAX_WEIGHT) {
return j;
}
}
return -1;
}
/**
圖的深度優先遍歷算法
*/
public void depthFirstSearch(int i) {
isVisited[i] = true;
int w = getFirstNeighbor(i);
while (w != -1) {
if (!isVisited[w]) {
// 需要遍歷該頂點
System.out.println("訪問到了 : " + w + "頂點");
depthFirstSearch(w);
}
// 第一個相對於w的鄰接點
w = getNextNeighbor(i, w);
}
}
/**
對外公開的深度優先遍歷
*/
public void depthFirstSearch() {
isVisited = new Boolean[vertextSize];
for (int i = 0; i < vertexSize; i++) {
if (isVisited[i]) {
depthFirstSearch(i);
}
}
isVisited = new Boolean[vertexSize];
}
public void broadFirstSearch() {
isVisited = new boolean[vertexSize];
for (int i = 0; i < vertexSize; i++) {
if (!isVisited[i]) {
broadFirstSearch(i);
}
}
}
/**
實現廣度優先遍歷
*/
public void broadFirstSearch(int i) {
int u,w;
LinkedList<Integer> queue = new LinkedList<Integer>();
System.out.println("訪問到 : " + i + "頂點");
isVisited[i] = true;
queue.add(i); // 第一次把V0加到隊列
while (!queue.isEmpty()) {
u = (Integer)(queue.removeFirst()).intValue();
w = getFirstNeighbor(u);
while (w != -1) {
if (!isVisited[w]) {
System.out.println("訪問到了 : " + w + "頂點");
isVisited[w] = true;
queue.add(w);
}
w = getNextNeighbor(u, w);
}
}
}
}
最小生成樹
假設你是電信的實施工程師,需要為一個鎮的九個村庄架設通信網絡做設計,村庄位置大致如圖7-6-1,其中v0~v8是村庄,之間連線的數字表示村與村間的可通達的直線距離
進行設計?
一個連通圖的生成樹是一個極小的連通子圖,它含有圖中全部的頂點,但只有足以構成一棵樹的n-1條邊.我們把構造連通網的最小代價生成樹.稱為最小生成樹.
找連通網的最小生成樹,經典的有兩種算法,普里姆算法和克魯斯卡爾算法
例子 :
/**
prim 普里姆算法(先構造鄰接矩陣)
*/
public void prim() {
// 最小代價頂點權值的數組,為0表示已經頂點(自己)
int[] lowcost = new int[vertexSize];
// 放頂點權值
int[] adjvex = new int[vertexSize];
for (int i = 1; i < vertexSize; i++) {
lowcost[i] = matrix[0][i];
}
for (int i = 1; i < vertexSize; j++) {
min = MAX_WEIGHT;
minId = 0;
for (int j = 1; j < vertexSize; j++) {
if (lowcost[j] < min && lowcost[j] > 0) {
min = lowcost[j];
minId = j;
}
}
System.out.println("頂點 : " + adjvex[minId] + "權值 : " + min);
sum += min;
lowcost[minId] = 0;
for (int j = 1; j < vertexSize; j++) {
if (lowcost[j] != 0 && matrix[minId][j] < lowcost[j]) {
lowcost[j] = matrix[minId][j];
adjvex[j] = minId;
}
}
}
System.out.println("最小生成樹權值和 : " + sum);
}
克魯斯卡爾算法實現 :
例子 :
package com.example.demo.graph;
public class GraphKruskal {
private Edge[] edges;
private int edgeSize;
private GraphKruskal(int edgeSize) {
this.edgeSize = edgeSize;
edges = new Edge[edgeSize];
}
public void miniSpanTreeKruskal() {
int m,n,sum = 0;
// 神奇的數組,下標為起點,值為終點
int[] parent = new int[edgeSize];
for (int i = 0; i < edgeSize; i++) {
parent[i] = 0;
}
for (int i = 0; i < edgeSize; i++) {
n = find(parent, edges[i].begin);
m = find(parent, edges[i].end);
if (n != m) {
parent[n] = m;
System.out.println("起始頂點 : " + edges[i].begin + "---結束頂點 : " + edges[i].end + "~權值 : " + edges[i].weight);
sum += edges[i].weight;
} else {
System.out.println("第 " + i + "條邊回環了");
}
}
System.out.println("sum : " + sum);
}
/**
* 將神奇數組進行查詢獲取非回環的值
*/
public int find(int[] parent, int f) {
while(parent[f] > 0) {
System.out.println("找到起點 : " + f);
f = parent[f];
System.out.println("找到終點 : " + f);
}
return f;
}
public void createEdgeArray() {
Edge edge = new Edge(4, 7, 7);
Edge edge1 = new Edge(2, 8, 8);
Edge edge2 = new Edge(0, 1, 10);
Edge edge3 = new Edge(0, 5, 11);
Edge edge4 = new Edge(1, 8, 12);
Edge edge5 = new Edge(3, 7, 16);
Edge edge6 = new Edge(2, 8, 8);
}
class Edge {
private int begin;
private int end;
private int weight;
public Edge(int begin, int end, int weight) {
super();
this.begin = begin;
this.end = end;
this.weight = weight;
}
public int getBegin() {
return begin;
}
public void setBegin(int begin) {
this.begin = begin;
}
public int getEnd() {
return end;
}
public void setEnd(int end) {
this.end = end;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
}
}