數據結構與常用集合總結
數據結構(英語:data structure)是計算機中存儲、組織數據的方式。 數據結構是一種具有一定邏輯關系,在計算機中應用某種存儲結構,並且封裝了相應操作的數據元素集合。 它包含三方面的內容,邏輯關系、存儲關系及操作。 不同種類的數據結構適合於不同種類的應用,而部分甚至專門用於特定的作業任務。
簡單來說數據結構(英語:data structure)是數據的組織、管理和存儲格式, 其使用目的是為了高效地訪問和修改數據。
數據結構主要分為:數組(Array)、棧(Stack)、隊列(Queue)、鏈表(Linked List)、樹(Tree)、散列表(也叫哈希表)(Hash)、堆(Heap)、圖(Graph)。 數據結構又可以分為線性表(全名為線性存儲結構。使用線性表存儲數據的方式可以這樣理解,即“把所有數據用一根線兒串起來,再存儲到物理空間中”,包含一維數組、棧、隊列、鏈表)和非線性表
⚠️數據結構的存儲方式只有兩種:數組(順序存儲)和鏈表(鏈式存儲)
時間復雜度
在計算機科學中,算法的時間復雜度(Time complexity)是一個函數,它定性描述該算法的運行時間。這是一個代表算法輸入值的字符串的長度的函數。時間復雜度常用大O符號表述,不包括這個函數的低階項和首項系數。使用這種方式時,時間復雜度可被稱為是漸近的,亦即考察輸入值大小趨近無窮時的情況。
表示方法
「 大O符號表示法 」,即 T(n) = O(f(n)),在 大O符號表示法中,時間復雜度的公式是: T(n) = O( f(n) ),其中f(n) 表示每行代碼執行次數之和,而 O 表示正比例關系
常見的時間復雜度量級
- 常數階O(1)
- 對數階O(logN)
- 線性階O(n)
- 線性對數階O(nlogN)
- 平方階O(n²)
- 立方階O(n³)
- K次方階O(n^k)
- 指數階(2^n)
求解算法復雜度一般分以下幾個步驟
- 找出算法中的基本語句:算法中執行次數最多的語句就是基本語句,通常是最內層循環的循環體。
- 計算基本語句的執行次數的數量級:只需計算基本語句執行次數的數量級,即只要保證函數中的最高次冪正確即可,可以忽略所有低次冪和最高次冪的系數。這樣能夠簡化算法分析,使注意力集中在最重要的一點上:增長率。
- 用大Ο表示算法的時間性能:將基本語句執行次數的數量級放入大Ο記號中。
其中用大O表示法通常有三種規則
- 用常數1取代運行時間中的所有加法常數;
- 只保留時間函數中的最高階項;
- 如果最高階項存在,則省去最高階項前面的系數;
實例分析
下面介紹一下常用的實例有助於我們來理解時間復雜度
1.常數階O(1)
無論代碼執行了多少行,只要是沒有循環等復雜結構,那這個代碼的時間復雜度就都是O(1),實例如下
int i = 0;//執行一次
int j = 1;//執行一次
++i;//執行一次
j++;//執行一次
int m = i + j;//執行一次
int n = j - i;//執行一次
復制代碼
上述代碼在執行的時候,每行代碼執行次數都是一次,不會隨着問題規模n的變化而變化,它消耗的時間並不隨着某個變量的增長而增長,那么無論這類代碼有多長,即使有幾萬幾十萬行,都可以用O(1)來表示它的時間復雜度。
2.對數階O(logN)
先上代碼再來分析,這里n先是一個不確定的數
int i = 1;//執行一次
while(i<n)
{
i = i * 2;//執行log2^n
}
復制代碼
從上面代碼可以看出,在while循環里面,每次都將i*2然后重新賦值給i,乘完之后,i距離n會越來越近。假設我們在循環j次之后,i> n了,此時就退出當前的循環,也就是說2的j次方就等於n,那么j=log2^n,也就是說循環log2^n次以后,這個代碼就結束了。因此我們得到這段代碼的時間復雜度為 T(n)=O(logn)。
3.線性階O(n)
我們這里用一個經常用的代碼來分析
int j = 0;//執行1次
for(i=1; i<=n; i++)//執行n次
{
j = i;//執行n次
j++;//執行n次
}
復制代碼
這段代碼中for循環里面的代碼會執行n遍,因此它消耗的時間是隨着n的變化而變化的,因此這類代碼都可以用T(n)=O(n)來表示它的時間復雜度。
4.線性對數階O(nlogN)
線性對數階O(nlogN) 其實非常容易理解,將時間復雜度為O(logn)的代碼循環N遍的話,那么它的時間復雜度就是 n * O(logN),也就是了O(nlogN)。參考代碼如下
for(m=1; m<n; m++)//執行n次
{
i = 1;//執行n次
while(i<n)
{
i = i * 2;//執行n*logN次
}
}
復制代碼
首先while循環里面的時間復雜度就是對數階O(logN),和對數階O(logN)的實例一樣的道理,然后這個while循環會根據外層的for循環的執行會被執行n次,因此就是T(n)=O(nlogN)
5. 平方階O(n²)
平方階O(n²) 就更容易理解了,如果把 O(n) 的代碼再嵌套循環一遍,它的時間復雜度就是 O(n²) 了。典型的就是雙重for循環實例
for(i=1; i<=n; i++)//執行n次
{
for(j=1; j<=n; j++)//執行n*n次
{
k = j;//執行n*n次
k++;
}
}
復制代碼
這段代碼就是嵌套了兩層n循環,因此時間復雜度為O(n*n),即 T(n)=O(n²)
如果我們將外層for循環的n改成m,他的時間復雜度是多少了
for(i=1; i<=m; i++)//執行m次
{
for(j=1; j<=n; j++)//執行m*n次
{
k = j;
k++;
}
}
復制代碼
這段代碼第一層嵌套了m循環,第二層是n循環,因此時間復雜度為O(mn),即 T(n)=O(mn)
6. 立方階O(n³)
參照上面的平方階O(n²)的實例,我們能推測出立方階O(n³)就是嵌套了三層n循環,實例代碼如下
for(i=1; i<=n; i++)//執行n次
{
for(j=1; j<=n; j++)//執行n*n次
{
for(k=1; k<=n; k++)//執行n*n*n次
{
o = k;
o++;
}
}
}
復制代碼
這段代碼就是嵌套了三層n循環,因此時間復雜度為O(nnn),即 T(n)=O(n³)
7. K次方階O(n^k)
因此也能得到K次方階O(n^k)就是嵌套了k層n循環,這樣就不舉例了
8. 指數階(2^n)
指數階表現的最常用的場景就是求子集(給定一組 不含重復元素 的整數數組 nums,返回該數組所有可能的子集(冪集)),實例代碼如下
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> output = new ArrayList();
output.add(new ArrayList<Integer>());
for (int num : nums) {//執行n次
List<List<Integer>> newSubsets = new ArrayList();
for (List<Integer> curr : output) {//
newSubsets.add(new ArrayList<Integer>(curr){{add(num);}});
}
for (List<Integer> curr : newSubsets) {
output.add(curr);
}
}
return output;
}
復制代碼
時間復雜度為T(n)=O(n*2^n)
⚠️ 加法法則:總復雜度等於量級最大的那段代碼的復雜度
⚠️ 多段代碼取最大:比如一段代碼中有單循環和多重循環,那么取多重循環的復雜度。
空間復雜度
空間復雜度(Space Complexity)是對一個算法在運行過程中臨時占用存儲空間大小的量度,記做S(n)=O. (f(n))。 比如直接插入排序的時間復雜度是O(n^2),空間復雜度是O(1) 。 而一般的遞歸算法就要有O(n)的空間復雜度了,因為每次遞歸都要存儲返回信息。
表示方法
空間復雜度是對一個算法在運行過程中臨時占用存儲空間大小的一個量度,同樣反映的是一個趨勢,我們用 S(n) 來定義。空間復雜度 S(n) = O(f(n))
常見的時間復雜度量級
- 常數階O(1)
- 線性階O(n)
- 平方階O(n²)
實例分析
1.常數階O(1)
如果算法執行所需要的臨時空間不隨着某個變量n的大小而變化,即此算法空間復雜度為一個常量,可表示為 O(1)
int i = 0;
int j = 1;
++i;
j++;
int m = i + j;
int n = j - i;
復制代碼
代碼中的 i、j、m、n 所分配的空間都不隨着處理數據量變化,因此它的空間復雜度 S(n) = O(1)
2.線性階O(n)
直接上代碼來分析
int[] m = new int[n];//這行代碼占用的內存大小為n
for(i=1; i<=n; ++i)//下面的循環沒有分配新的空間
{
j = i;
j++;
}
復制代碼
這段代碼中,第一行new了一個數組出來,這個數據占用的大小為n,這段代碼的2-6行,雖然有循環,但沒有再分配新的空間,因此,這段代碼的空間復雜度主要看第一行即可,即 S(n) = O(n)
特殊的遞歸算法的空間復雜度
void fun1(int n){
if(n<=1){
return;
}
fun1(n-1);
}
復制代碼
假設我們這里傳入參數6,那么fun1(n=6)的調用信息先入棧。接下來遞歸調用相同的方法,fun1(n=5) 的調用信息入棧。以此類推,遞歸越來越深,入棧的元素也越來越多。當n=1時,達到遞歸結束條件,執行return指令,方法出棧。最終,“方法調用棧”的全部元素會一一出棧。 由上面“方法調用棧”的出入棧過程可以看出,執行遞歸操作所需要的內存空間和遞歸的深度成正比。純粹的遞歸操作的空間復雜度也是線性的,如果遞歸的深度是n,那么空間復雜度就是O(n),即S(n)=O(n)。
3.平方階O(n²)
int[][] matrix = new int[n][n];
復制代碼
這段代碼中二維數組就是n*n,即S(n) = O(n²)
⚠️⚠️⚠️一個算法中,考量一個算法的好壞都是從時間復雜度和空間復雜度上去對比考量的,最少時間最小空間肯定是最好的,有時候要根據具體情況去做時間復雜度和空間復雜度的取舍。在絕大多數時候,時間復雜度更為重要一些,我們寧可多分配一些內存空間,也要提升程序的執行速度。
數組(Array)
數組是可以在內存中連續存儲多個元素的結構,在內存中的分配也是連續的,數組中的元素通過數組下標進行訪問,數組下標從0開始。數組在內存中是順序存儲的,因此可以很好的實現邏輯上的順序表。
數組在內存中順序存儲
內存是由一個個連續的內存單元組成的,每一個內存單元都有自己的地址。在 這些內存單元中,有些被其他數據占用了,有些是空閑的。 數組中的每一個元素,都存儲在小小的內存單元中,並且元素之間緊密排列, 既不能打亂元素的存儲順序,也不能跳過某個存儲單元進行存儲。
實例講解
int[] nums=new int[]{3,1,2,5,4,9,7,2};
復制代碼
在上圖中,橙色的格子代表空閑的存儲單元,灰色的格子代表已占用的存儲單元,而紅色的連續格子代表數組在內存中的位置。不同類型的數組,每個元素所占的字節個數也不同,本圖只是一個簡單的示意圖。
數組的基本操作
數組的增刪改查
- 首先說增:數組中增加新數據得先判斷插入的下角標是否超出數組范圍,超出的話拋異常; 再判斷這個數組的大小是否已滿,滿的話進行當前數組大小的2倍進行擴容,擴容后進行重新賦值; 然后再是真正的插入了,插入的話,需要將當前位置及其后面位置的數據向后移動一位,然后把新數據插入到當前位置,先移再插。
- 然后再說刪:數組的刪除的邏輯和增加邏輯相似,只不過是先刪再移動
- 然后再說改:改的話,判斷一下修改的index位置是否存在,不存在拋異常,存在的話就直接修改賦值
- 最好說查:查的話,判斷一下修改的index位置是否存在,不存在拋異常,存在的話就直接查詢返回值
我們來根據這個思路來用代碼實現一下數組的增刪改查
public class DataStructureArray {
/**
* 數組增刪改查操作的代碼實現
* 3,1,2,
*
* @param args
*/
public static void main(String[] args) {
MyArray myArray = new MyArray(2);
//增
myArray.add(0, 3);
myArray.add(1, 1);
myArray.add(2, 2);
System.out.print("\n增 ");
myArray.outPut();
//刪
// myArray.delete(3);//數組越界
myArray.delete(1);
System.out.print("\n刪 ");
myArray.outPut();
//改
// myArray.update(2, 1);//數組越界
myArray.update(1, 1);
System.out.print("\n改 ");
myArray.outPut();
//查
System.out.print("\n查 "+myArray.get(0));
}
public static class MyArray {
private int[] myArray;
private int size;
public MyArray(int capacity) {
this.myArray = new int[capacity];
size = 0;
}
/**
* 增
*
* @param index
* @param element
*/
private void add(int index, int element) {
if (0 <= index && index <= size) {
//先判斷一下是否需要擴容
if (size >= myArray.length) {
//數組擴容
resize();
}
//從index位置開始移動,從右向左循環,將元素逐個向右移動一位
for (int i = size - 1; i >= index; i--) {
myArray[i + 1] = myArray[i];
}
myArray[index] = element;
size++;
} else {
throw new IndexOutOfBoundsException("超出數組實際元素范圍");
}
}
/**
* 數組擴容
*/
private void resize() {
//我們這里采取的是2倍擴容
int[] myNewArray = new int[myArray.length * 2];
//數據copy,舊數組數據復制到新數組上面去
System.arraycopy(myArray, 0, myNewArray, 0, myArray.length);
//再賦值
myArray = myNewArray;
}
/**
* 刪
*
* @param index
* @return
*/
private int delete(int index) {
if (0 <= index && index < size) {
//得到刪除的元素
int deleteElement = myArray[index];
//然后再將index位置右邊元素向左移動一位
for (int i = index; i < size - 1; i++) {
myArray[i] = myArray[i + 1];
}
size--;
return deleteElement;
} else {
throw new IndexOutOfBoundsException("超出數組實際元素范圍");
}
}
/**
* 改
*
* @param index
* @param element
*/
private void update(int index, int element) {
if (0 <= index && index < size) {
myArray[index] = element;
} else {
throw new IndexOutOfBoundsException("超出數組實際元素范圍");
}
}
/**
* 查
*
* @param index
* @return
*/
private int get(int index) {
if (0 <= index && index < size) {
return myArray[index];
} else {
throw new IndexOutOfBoundsException("超出數組實際元素范圍");
}
}
/**
* 輸出數據
*/
private void outPut() {
for (int i = 0; i < size; i++) {
System.out.print(myArray[i]);
}
}
}
}
復制代碼
數組的優缺點
從代碼的實現上可以看出來,數組擁有非常高效的查詢能力,給出下角標就能很快的查詢到對應元素,說到這里,就要提一下二分查找法,在刷算法的時候會經常用到這個算法思想去解決實際問題;反之,我們從代碼實現層面也能看出來數組的增刪是很耗時,每一次都要做數據的移動。因此我們總結到數組是適合做改查操作、不適合做增刪操作的。
數組實現的常用集合
ArrayList就是一個動態修改的數組,我們可以查看一下它的源碼來加深一下對數組的實現的理解 Vector也是一個動態數組,它是同步的,是一個可以改變大小的數組。
鏈表(Linked List)
鏈表是一種物理存儲單元上非連續、非順序的存儲結構,數據元素的邏輯順序是通過鏈表中的指針鏈接次序實現的。 鏈表由一系列結點(鏈表中每一個元素稱為結點)組成,結點可以在運行時動態生成。 每個結點包括兩個部分:一個是存儲數據元素的數據域,另一個是存儲下一個結點地址的指針域。 相比於線性表順序結構,操作復雜。
鏈表在內存中的存儲
鏈表則采用了見縫插針的方式,鏈表的每一個節點分布在內存的不同位 置,依靠next指針關聯起來。這樣可以靈活有效地利用零散的碎片空間。
和上面數組相同的數據,鏈表的存儲方式如下
鏈表的基本操作
鏈表的增刪改查
- 首先說增:鏈表中增加數據,得先判斷是不是空鏈表,是空的話,直接賦值; 插入元素插入的位置是頭部,將插入的節點的next指向當前的頭節點,然后將頭節點設置為當前值 插入元素插入的位置是尾部,將尾部節點的next指向當前節點,然后再設置尾部節點為當前值 插入元素插入的位置是中間,先拿到插入位置的前一個節點,設置插入節點的下一個是當前位置這個節點(即前一個節點的next節點),再設置前一個的節點next是當前插入的
- 然后再說刪:鏈表的刪除的邏輯和增加邏輯相似
- 然后再說改:改的話,先查到當前這個節點,然后設置當前節點的值
- 最好說查:查的話,得從頭節點開始查,頭節點的next,再next,一直查到為止
我們來根據這個思路來用代碼實現一下鏈表的增刪改查
public class DataStructureLinkedList {
/**
* 鏈表增刪改查操作的代碼實現
* 3,5,7,
* 關聯常用數組:LinkedList
*
* @param args
*/
public static void main(String[] args) {
MyLinked myLinked = new MyLinked();
myLinked.add(0, 3);
myLinked.add(1, 5);
myLinked.add(2, 7);
System.out.print("\n增 ");
myLinked.output();
// myLinked.delete(0);
myLinked.delete(1);
// myLinked.delete(2);
System.out.print("\n刪 ");
myLinked.output();
myLinked.update(0,9);
System.out.print("\n改 ");
myLinked.output();
}
public static class MyLinked {
private Node head;
private Node last;
private int size;
public class Node {
int data;
Node next;
public Node(int data) {
this.data = data;
}
}
/**
* 增
*
* @param index
* @param element
*/
private void add(int index, int element) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException("超出鏈表節點范圍");
}
Node currentNode = new Node(element);
//頭部插入
if (index == 0) {
//空鏈表
if (head == null) {
head = currentNode;
last = currentNode;
//頭部插入
} else {
currentNode.next = head;
head = currentNode;
}
//尾部插入
} else if (index == size) {
last.next = currentNode;
last = currentNode;
//中間插入
} else {
Node preNode = get(index - 1);
currentNode.next = preNode.next;
preNode.next = currentNode;
}
size++;
}
/**
* 刪
*
* @param index
* @return
*/
private Node delete(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("超出鏈表節點范圍");
}
Node deleteNode = null;
//頭節點刪除
if (index == 0) {
deleteNode = head;
head = head.next;
//尾節點刪除
} else if (index == size - 1) {
Node preNode = get(index - 1);
deleteNode = last;
preNode.next = null;
last = preNode;
//中間節點刪除
} else {
Node preNode = get(index - 1);
preNode.next = preNode.next.next;
deleteNode = preNode.next;
}
size--;
return deleteNode;
}
/**
* 改
*
* @param element
*/
private void update(int index, int element) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("超出鏈表節點范圍");
}
Node node = get(index);
node.data = element;
}
/**
* 查
*
* @param index
* @return
*/
private Node get(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("超出鏈表節點范圍");
}
Node temp = head;
for (int i = 0; i < index; i++) {
temp = temp.next;
}
return temp;
}
private void output() {
Node temp = head;
while (temp != null) {
System.out.print(temp.data);
temp = temp.next;
}
}
}
}
復制代碼
鏈表的優缺點
鏈表是寫操作快,讀操作慢,實際應用場景中,那些需要頻繁增刪數據的就適合用鏈表去實現
鏈表實現的常用集合
LinkedList就是一個雙向鏈表,我們可以查看一下它的源碼來加深一下對鏈表的實現的理解
⚠️⚠️⚠️說到這里,我們就介紹完了數組和鏈表,其實️數據結構的存儲方式只有兩種:數組(順序存儲)和鏈表(鏈式存儲)。
棧(Stack)
堆棧(英語:stack)又稱為棧或堆疊,是計算機科學中的一種抽象資料類型,只允許在有序的線性資料集合的一端(稱為堆棧頂端,英語:top)進行加入數據(英語:push)和移除數據(英語:pop)的運算。因而按照后進先出(LIFO, Last In First Out)的原理運作。
常與另一種有序的線性資料集合隊列相提並論。下面我們也會講到。
棧就像一個裝羽毛球的球筒(一端封閉,另一端開口),往圓筒里放入羽毛球,先放入的靠近圓筒底部,后放入的靠近圓筒入口。這就相當於是一個push入棧的過程。那么,要想取出這些羽毛球,則只能按照和放入順序相反的順序來取,先取出 后放入的,再取出先放入的,而不可能把最里面最先放入的羽毛球優先取出。這就相當於一個pop出棧的過程。
棧的基本操作
棧的基本操作只有入棧和出棧,入棧操作(push)就是把新元素放入棧中,只允許從棧頂一側放入元素,新元素的位置將會成為新的棧頂。出棧操作(pop) 就是把元素從棧中彈出,只有棧頂元素才允許出棧,出棧元素的前一個元素將會成為新的棧頂。我們接下來分別用數組和鏈表來實現棧。
數組實現棧
import java.util.Stack;
public class DataStructureUseArrayRealizeStack {
/**
* 這里用數組來實現棧
*
* @param args
*/
public static void main(String[] args) {
MyStack myStack = new MyStack(3);
myStack.push(1);
myStack.push(2);
myStack.push(3);
System.out.print("\n入棧 ");
myStack.output();
// myStack.push(4);//超出棧的范圍大小
myStack.pop();
myStack.pop();
myStack.pop();
System.out.print("\n出棧 ");
myStack.output();
// myStack.pop();//棧內無數據
}
public static class MyStack {
private int[] data;
private int size;
private int topIndex;
public MyStack(int size) {
data = new int[size];
this.size = size;
topIndex = -1;
}
private void push(int element) {
if (isFull()) {
throw new IndexOutOfBoundsException("超出棧的范圍大小");
} else {
data[topIndex + 1] = element;
topIndex++;
}
}
private int pop() {
if (isEmpty()) {
throw new IndexOutOfBoundsException("棧內無數據");
} else {
int[] newdata = new int[data.length - 1];
for (int i = 0; i < data.length - 1; i++) {
newdata[i] = data[i];
}
int element = data[topIndex];
topIndex--;
data = newdata;
return element;
}
}
private boolean isFull() {
return data.length - 1 == topIndex;
}
private boolean isEmpty() {
return topIndex == -1;
}
private void output() {
for (int i = 0; i < data.length; i++) {
System.out.print(data[i]);
}
}
}
}
復制代碼
鏈表實現棧
public class DataStructureUseLinkeListdRealizeStack {
/**
* 用鏈表去實現棧
* 我們首先要實現鏈表,然后再根據鏈表去實現棧
*
* @param args
*/
public static void main(String[] args) {
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.push(3);
System.out.print("\n入棧 ");
myStack.output();
myStack.pop();
myStack.pop();
System.out.print("\n出棧 ");
myStack.output();
myStack.pop();
// myStack.pop();//棧內無數據
}
public static class MyStack {
private LinkedList linkedList;
private MyStack() {
linkedList = new LinkedList();
}
private void push(int element) {
linkedList.addToFirst(element);
}
private int pop() {
if (linkedList.isEmpty()) {
throw new IndexOutOfBoundsException("棧內無數據");
} else {
return linkedList.deleteFirst();
}
}
private void output() {
linkedList.output();
}
/**
* 節點
*/
private class Node {
private int data;
private Node next;
private Node(int data) {
this.data = data;
}
}
/**
* 鏈表
*/
private class LinkedList {
private Node first;
private LinkedList() {
first = null;
}
private boolean isEmpty() {
return first == null;
}
/**
* @param element
*/
private void addToFirst(int element) {
Node newNode = new Node(element);
newNode.next = first;
first = newNode;
}
private int deleteFirst() {
Node firstNode = first;
first = first.next;
return firstNode.data;
}
private void output() {
Node currentNode = first;
while (currentNode != null) {
System.out.print(currentNode.data);
currentNode = currentNode.next;
}
}
}
}
}
復制代碼
棧的特點
優點:由於棧中存放數據的結構是后放進去的數據先取出來(后進先出),針對一些操作需要取最新數據時,選擇棧作為數據結構是最合適的。
缺點:訪問棧中的任意數據時,就需要從最新的數據開始取,效率較低。
棧實現的常用集合
Stack是Vector的一個子類,它實現了一個標准的后進先出的棧。
隊列(Queue)
隊列,又稱為佇列(queue),計算機科學中的一種抽象資料型別,是先進先出(FIFO, First-In-First-Out)的線性表。在具體應用中通常用鏈表或者數組來實現。隊列只允許在后端(稱為rear)進行插入操作,在前端(稱為front)進行刪除操作。
隊列的操作方式和堆棧類似,唯一的區別在於隊列只允許新數據在后端進行添加。
隊列就像公路上有一條單行隧道,所有通過隧道的車輛只允許從隧道入口駛入,從隧道出口駛出,不允許逆行。因此,要想讓車輛駛出隧道,只能按照它們駛入隧道的順序,先駛入的車輛先駛出,后駛入的車輛后駛出,任何車輛都無法跳過它前面的車輛提前駛出。
隊列的基本操作
隊列的基本操作只要入隊和出隊。入隊(enqueue)就是把新元素放入隊列中,只允許在隊尾的位置放入元素, 新元素的下一個位置將會成為新的隊尾。出隊操作(dequeue) 就是把元素移出隊列,只允許在隊頭一側移出元素,出隊元素的后一個元素將會成為新的隊頭。隊列也是數組和鏈表都能實現隊列的這種操作,我們也分別從數組和鏈表兩種方案來實現這個隊列。
數組實現隊列
public class DataStructureUseArrayRealizeQueue {
/**
* 這里用數組來實現隊列(循環隊列)
*
* @param args
*/
public static void main(String[] args) {
MyQueue myQueue = new MyQueue(4);
myQueue.enQueue(1);
myQueue.enQueue(2);
myQueue.enQueue(3);
myQueue.enQueue(4);
System.out.print("\n入隊 ");
myQueue.output();
// myQueue.enQueue(5);// 超出隊列的長度
myQueue.deQueue();
myQueue.deQueue();
System.out.print("\n出隊 ");
myQueue.output();
myQueue.enQueue(6);
System.out.print("\n再入隊 ");
myQueue.output();
myQueue.deQueue();
myQueue.deQueue();
myQueue.deQueue();
// myQueue.deQueue();// 隊列已空
}
public static class MyQueue {
private int[] data;
private int front;//隊頭
private int rear;//隊尾
private MyQueue(int capacity) {
this.data = new int[capacity + 1];//隊尾指針指向的位置永遠空出1位,所以隊列最大容量比數組長度小 1。因此我們這里實現的時候設置數組長度的時候用capacity + 1
front = rear = 0;
}
/**
* 入隊
*
* @param element
*/
private void enQueue(int element) {
if (isFull()) {
throw new IndexOutOfBoundsException("超出隊列的長度");
} else {
data[rear] = element;
rear = (rear + 1) % data.length;//這里是循環隊列,所以不是直接rear++,而是通過數組的循環找到下一個隊尾下角標
}
}
/**
* 隊列滿了,隊尾下標與數組長度相除取余數和隊頭下標是否相等來判斷是不是隊列已滿
*
* @return
*/
private boolean isFull() {
return (rear + 1) % data.length == front;
}
/**
* 出隊
*/
private int deQueue() {
if (isEmpty()) {
throw new IndexOutOfBoundsException("隊列已空");
} else {
int element = data[front];
front = (front + 1) % data.length;
return element;
}
}
/**
* 空
*
* @return
*/
private boolean isEmpty() {
return front == rear;
}
private void output() {
//從頭開始,這里的累加是循環的
for (int i = front; i != rear; i = (i + 1) % data.length) {
System.out.print(data[i]);
}
}
}
}
復制代碼
鏈表實現隊列
public class DataStructureUseLinkeListdRealizeQueue {
/**
* 用鏈表去實現隊列
*
* @param args
*/
public static void main(String[] args) {
MyQueue myQueue = new MyQueue();
myQueue.enQueue(1);
myQueue.enQueue(2);
myQueue.enQueue(3);
myQueue.enQueue(4);
System.out.print("\n入隊 ");
myQueue.output();
myQueue.deQueue();
myQueue.deQueue();
System.out.print("\n出隊 ");
myQueue.output();
myQueue.enQueue(6);
System.out.print("\n再入隊 ");
myQueue.output();
myQueue.deQueue();
myQueue.deQueue();
myQueue.deQueue();
System.out.print("\n再出隊 ");
myQueue.output();
}
public static class MyQueue {
private Node head;
private Node rear;
private int size;
private MyQueue() {
head = null;
rear = null;
size = 0;
}
private void enQueue(int element) {
Node newNode = new Node(element);
if (isEmpty()) {
head = newNode;
} else {
rear.next = newNode;
}
rear = newNode;
size++;
}
private boolean isEmpty() {
return head == null;
}
private int deQueue() {
if (isEmpty()) {
throw new NullPointerException("隊列無數據");
} else {
Node node = head;
head = head.next;
size--;
return node.data;
}
}
private void output() {
Node node = head;
while (node != null) {
System.out.print(node.data);
node = node.next;
}
}
/**
* 節點
*/
private class Node {
private int data;
private Node next;
private Node(int data) {
this.data = data;
}
}
}
}
復制代碼
隊列的特點
隊列是一種比較特殊的線性結構。它只允許在表的前端(front)進行刪除操作,而在表的后端(rear)進行插入操作。進行插入操作的端稱為隊尾,進行刪除操作的端稱為隊頭。 隊列中最先插入的元素也將最先被刪除,對應的最后插入的元素將最后被刪除。因此隊列又稱為“先進先出”(FIFO—first in first out)的線性表,與棧(FILO-first in last out)剛好相反。
隊列實現的常用集合
- 雙端隊列:Deque
- 未實現阻塞接口的:
- LinkedList : 實現了Deque接口,受限的隊列
- PriorityQueue : 優先隊列,本質維護一個有序列表。可自然排序亦可傳遞 comparator構造函數實現自定義排序。
- ConcurrentLinkedQueue:基於鏈表 線程安全的隊列。增加刪除O(1) 查找O(n)
- 實現阻塞接口的:實現BlockingQueue接口的五個阻塞隊列,其特點:線程阻塞時,不是直接添加或者刪除元素,而是等到有空間或者元素時,才進行操作。
- ArrayBlockingQueue: 基於數組的有界隊列
- LinkedBlockingQueue: 基於鏈表的無界隊列
- PriorityBlockingQueue:基於優先次序的無界隊列
- DelayQueue:基於時間優先級的隊列
- SynchronousQueue:內部沒有容器的隊列 較特別 --其獨有的線程一一配對通信機制
隊列主要任務場景就是任務的調度管控、處理並發任務
散列表(也叫哈希表)(Hash)
散列表(Hash table,也叫哈希表),是根據鍵(Key)而直接訪問在內存儲存位置的數據結構。也就是說,它通過計算一個關於鍵值的函數,將所需查詢的數據映射到表中一個位置來訪問記錄,這加快了查找速度。這個映射函數稱做散列函數,存放記錄的數組稱做散列表。
一個通俗的例子是,為了查找電話簿中某人的號碼,可以創建一個按照人名首字母順序排列的表(即建立人名{\displaystyle x}x到首字母{\displaystyle F(x)}F(x) 的一個函數關系),在首字母為W的表中查找“王”姓的電話號碼,顯然比直接查找就要快得多。這里使用人名作為關鍵字,“取首字母”是這個例子中散列函數的函數法則{\displaystyle F()}F() ,存放首字母的表對應散列表。關鍵字和函數法則理論上可以任意確定。
散列函數(也叫哈希函數)
散列函數(英語:Hash function)又稱散列算法、哈希函數,是一種從任何一種數據中創建小的數字“指紋”的方法。散列函數把消息或數據壓縮成摘要,使得數據量變小,將數據的格式固定下來。該函數將數據打亂混合,重新創建一個叫做散列值(hash values,hash codes,hash sums,或hashes)的指紋。散列值通常用一個短的隨機字母和數字組成的字符串來代表。[1]好的散列函數在輸入域中很少出現散列沖突。在散列表和數據處理中,不抑制沖突來區別數據,會使得數據庫記錄更難找到。
如今,散列算法也被用來加密存在數據庫中的密碼(password)字符串,由於散列算法所計算出來的散列值(Hash Value)具有不可逆(無法逆向演算回原本的數值)的性質,因此可有效的保護密碼。
- 常見的散列函數
- 直接定址法:取關鍵字或關鍵字的某個線性函數值為散列地址。即hash(k)=k或hash(k)=a*k+b,其中a,b為常數(這種散列函數叫做自身函數)
- 數字分析法:假設關鍵字是以r為基的數,並且哈希表中可能出現的關鍵字都是事先知道的,則可取關鍵字的若干數位組成哈希地址。
- 折疊法:將關鍵字分割成位數相同的幾部分(最后一部分的位數可以不同),然后取這幾部分的疊加和(舍去進位)作為哈希地址。
- 隨機數法:使用rand()等隨機函數構造。
- 除留余數法:取關鍵字被某個不大於散列表表長m的數p除后所得的余數為散列地址。即hash(k)=k mod p,p< =m。不僅可以對關鍵字直接取模,也可在折疊法、平方取中法等運算之后取模。對p的選擇很重要,一般取素數或m,若p選擇不好,容易產生沖突。
解決哈希沖突的方法
-
開放地址法:key哈希后,發現該地值已被占用,可對該地址不斷加1,直到遇到一個空的地址。
-
再哈希法:發生“碰撞”后,可對key的一部份再進行哈希處理。
-
鏈地址法:鏈地址法是通過將key映射在同一地址上的value,做成一個鏈表
哈希表的基本操作
哈希表的基本操作涉及到增刪改查方法,我們一個一個來分析
- 增(put)/改:就是在散列表中插入新的鍵值對(在JDK中叫作Entry)。例如:我們要調用hash.put("001","張三"),意思就是插入一組key=001.value="張三"的鍵值對。我們來分析一下具體步驟, 首先我們要通過哈希函數把key=001轉化成數組下標index;如果這時候下標index對應的位置沒有元素,我們就把這個Entry填充到數組下標index的值的位置; 這時候我們思考一下,因為數組長度有限,當插入的Entry越來越多的時候,不同key做哈希運算得到的下標可能是相同的,當出現相同下標index的時候,我們該怎么處理這種情況了,這就涉及到了哈希沖突,我們上面也列舉了解決哈希沖突的方法。HashMap中采用的是鏈地址法。 這時候我們再思考一下,因為數組在實例化的時候有設置數組長度,再反觀一下鏈表的查詢在數據量大的時候就會很慢。當散列表存的鍵值對越來越多的時候,我們將要考慮擴容,說到擴容就涉及到負載因子( loadFactor負載因子是和擴容機制有關的,意思是如果當前容器的容量,達到了我們設定的最大值,就要開始執行擴容操作。 舉個例子來解釋,避免小白聽不懂: 比如說當前的容器容量是16,負載因子是0.75,16* 0.75=12,也就是說,當容量達到了12的時候就會進行擴容操作。 他的作用很簡單,相當於是一個擴容機制的閾值)
- 查(get):讀操作就是通過給定的Key,在散列表 中查找對應的Value。例如:我們要調用hash.get("001"),意思是查找Key=001的Entry在散列表中所對應的值。我們也來分析一下具體怎么做 通過哈希函數,把Key轉化成數組下標index;找到數組下標index所對應的元素,如果這個元素的Key=001,那么就找到了;如果這個Key不是001也沒關系,由於數組的每個元素都與一個鏈表對應,我們可以順着鏈表慢慢往下找,看看能否找到與Key相匹配的節點。
- 刪(remove):刪除的話也是通過查找key的哈希函數找到對應的下標再進行刪除
public class DataStructureHash {
/**
* 這里我們就是簡單的實現了一個散列表,具體的全方面實現去參考HashMap
*
* @param args
*/
public static void main(String[] args) {
MyHash myHash = new MyHash();
myHash.put(0, 1);
myHash.put(1, 2);
myHash.get(0);
myHash.remove(0);
}
public static class MyHash {
private static final int DEFAULT_INITAL_CAPACITY = 5;//定義的是默認長度
private static final float LOAD_FACTOR = 0.75f;//負載因子
private Entry[] table = new Entry[DEFAULT_INITAL_CAPACITY];//初始化
private int size = 0;//哈系表大小
private int use = 0;//使用的地址數量
private class Entry {
int key;//關鍵字
int value;
Entry next;//鏈表
public Entry(int key, int value, Entry entry)//構造函數
{
this.key = key;
this.value = value;
this.next = entry;
}
}
/**
*
* @param key
* @param value
*/
public void put(int key, int value) {
int index = hash(key);//通過hash方法轉換,采用的是直接法
if (table[index] == null)//說明位置未被使用
{
table[index] = new Entry(-1, -1, null);
}
Entry tmp = table[index];
if (tmp.next == null)//說明位置未被使用
{
table[index].next = new Entry(key, value, null);
size++;
use++;
if (use >= table.length * LOAD_FACTOR)//判斷是否需要擴容
{
resize();//擴容方法
}
} else {//已被使用,則直接擴展鏈表
for (tmp = tmp.next; tmp != null; tmp = tmp.next) {
int k = tmp.key;
if (k == key) {
tmp.value = value;
return;
}
}
Entry temp = table[index].next;
Entry newEntry = new Entry(key, value, temp);
table[index].next = newEntry;
size++;
}
}
/**
*
* 刪除,鏈表的中間值刪除方法
* @param key
*/
public void remove(int key)
{
int index = hash(key);
Entry e = table[index];
Entry pre = table[index];
if (e != null && e.next != null) {
for (e = e.next; e != null; pre = e, e = e.next) {
int k = e.key;
if (k == key) {
pre.next = e.next;
size--;
return;
}
}
}
}
/**
* 通過key提取value
* @param key
* @return
*/
public int get(int key)
{
int index = hash(key);
Entry e = table[index];
if (e != null && e.next != null) {
for (e = e.next; e != null; e = e.next) {
int k = e.key;
if (k == key) {
return e.value;
}
}
}
return -1;
}
/**
* 返回元素個數
* @return
*/
public int size() {
return size;
}
/**
* 哈希表大小
* @return
*/
public int getLength() {
return table.length;
}
/**
* 擴容
*/
private void resize() {
int newLength = table.length * 2;
Entry[] oldTable = table;
table = new Entry[newLength];
use = 0;
for (int i = 0; i < oldTable.length; i++) {
if (oldTable[i] != null && oldTable[i].next != null) {
Entry e = oldTable[i];
while (null != e.next) {
Entry next = e.next;
int index = hash(next.key);
if (table[index] == null) {
use++;
table[index] = new Entry(-1, -1, null);
}
Entry temp = table[index].next;
Entry newEntry = new Entry(next.key, next.value, temp);
table[index].next = newEntry;
e = next;
}
}
}
}
/**
* 得到key的下標(哈希函數)
* @param key
* @return
*/
private int hash(int key) {
return key % table.length;
}
}
}
復制代碼
哈希表的優缺點
優點:哈希表不僅速度快,編程實現也相對容易
缺點:哈希表也有一些缺點它是基於數組的,數組創建后難於擴展某些哈希表被基本填滿時,性能下降得非常嚴重,所以程序雖必須要清楚表中將要存儲多少數據(或者准備好定期地把數據轉移到更大的哈希表中,這是個費時的過程)。 而且,也沒有一種簡便的方法可以以任何一種順序〔例如從小到大〕遍歷表中數據項。
哈希表實現的常用集合
HashMap、HashTable、ConcurrentHashMap
樹(Tree)
樹(英語:tree)是一種抽象數據類型(ADT)或是實現這種抽象數據類型的數據結構,用來模擬具有樹狀結構性質的數據集合。它是由n(n> 0)個有限節點組成一個具有層次關系的集合。把它叫做“樹”是因為它看起來像一棵倒掛的樹,也就是說它是根朝上,而葉朝下的。它具有以下的特點:
-
每個節點都只有有限個子節點或無子節點;
-
沒有父節點的節點稱為根節點;
-
每一個非根節點有且只有一個父節點;
-
除了根節點外,每個子節點可以分為多個不相交的子樹;
-
樹里面沒有環路(cycle)
-
樹的相關術語
- 節點的度:一個節點含有的子樹的個數稱為該節點的度;
- 樹的度:一棵樹中,最大的節點度稱為樹的度;
- 葉節點或終端節點:度為零的節點;
- 非終端節點或分支節點:度不為零的節點;
- 父親節點或父節點:若一個節點含有子節點,則這個節點稱為其子節點的父節點;
- 孩子節點或子節點:一個節點含有的子樹的根節點稱為該節點的子節點;
- 兄弟節點:具有相同父節點的節點互稱為兄弟節點;
- 節點的層次:從根開始定義起,根為第1層,根的子節點為第2層,以此類推;
- 深度:對於任意節點n,n的深度為從根到n的唯一路徑長,根的深度為0;
- 高度:對於任意節點n,n的高度為從n到一片樹葉的最長路徑長,所有樹葉的高度為0;
- 堂兄弟節點:父節點在同一層的節點互為堂兄弟;
- 節點的祖先:從根到該節點所經分支上的所有節點;
- 子孫:以某節點為根的子樹中任一節點都稱為該節點的子孫。
- 森林:由m(m>=0)棵互不相交的樹的集合稱為森林;
常見的樹
二叉樹(binary tree)是樹的一種特殊形式。二叉,顧名思義,這種樹的每個節點最多有2個孩子節點。注意,這里是最多有2個,也可能只有1個,或者沒有孩子節點。二叉樹節點的兩個孩子節點,一個被稱為左孩子(left child) ,一個被稱為右孩 子(right child)。這兩個孩子節點的順序是固定的,就像人的左手就是左手,右手 就是右手,不能夠顛倒或混淆。
滿二叉樹:一個二叉樹的所有非葉子節點都存在左右孩子,並且所有葉子節點都在同一層級上,那么這個樹就是滿二叉樹。
完全二叉樹:對一個有n個節點的二叉樹,按層級順序編號,則所有節點的編號為從1到n。如果這個樹所有節點和同樣深度的滿二叉樹的編號為從1到n的節點位置相同,則這個二叉樹為完全二叉樹。
AVL樹:AVL樹是最先發明的自平衡二叉查找樹。 在AVL樹中任何節點的兩個子樹的高度最大差別為1,所以它也被稱為高度平衡樹。 增加和刪除可能需要通過一次或多次樹旋轉來重新平衡這個樹。
二叉樹包含許多特殊的形式,每一種形式都有自己的作用,但是其最主要的應用還在於進行查找操作和維持相對順序這兩個方面。
二叉樹的遍歷
從更宏觀的角度來看,二叉樹的遍歷歸結為兩大類。
- 深度優先遍歷(前序遍歷[---輸出順序是根節點、左子樹、右子樹---]、中序遍歷[---輸出順序是左子樹、根節點、右子樹---]、后序遍歷[---輸出順序是左子樹、右子樹、根節點---])
- 廣度優先遍歷(層序遍歷[---層序遍歷,顧名思義,就是二叉樹按照從根節點到葉子節點的層次關系,一層一層橫向遍歷各個節點。---])
樹實現的常用集合
TreeMap、TreeSet
堆(Heap)
堆(英語:Heap)是計算機科學中的一種特別的完全二叉樹。若是滿足以下特性,即可稱為堆:“給定堆中任意節點P和C,若P是C的母節點,那么P的值會小於等於(或大於等於)C的值”。若母節點的值恆小於等於子節點的值,此堆稱為最小堆(min heap);反之,若母節點的值恆大於等於子節點的值,此堆稱為最大堆(max heap)。在堆中最頂端的那一個節點,稱作根節點(root node),根節點本身沒有母節點(parent node)。
堆的優缺點
優點:插入,刪除快,對最大數據的項存取很快
缺點:對其他數據項存取很慢
堆的實際應用
二叉堆是實現堆排序及優先隊列的基礎,注意一些優先隊列在項目中的實際應用
堆實現的常用集合
暫無
圖(Graph)
圖(英語:graph)是一種抽象數據類型,用於實現數學中圖論的無向圖和有向圖的概念。
圖的數據結構包含一個有限(可能是可變的)的集合作為節點集合,以及一個無序對(對應無向圖)或有序對(對應有向圖)的集合作為邊(有向圖中也稱作弧)的集合。節點可以是圖結構的一部分,也可以是用整數下標或引用表示的外部實體。
圖的數據結構還可能包含和每條邊相關聯的數值(edge value),例如一個標號或一個數值(即權重,weight;表示花費、容量、長度等)。
圖的優缺點
優點:對現實世界建模
缺點:有些算法慢且復雜
圖的常見數據結構
鄰接表:節點存儲為記錄或對象,且為每個節點創建一個列表。這些列表可以按節點存儲其余的信息;例如,若每條邊也是一個對象,則將邊存儲到邊起點的列表上,並將邊的終點存儲在邊這個的對象本身。
鄰接矩陣:一個二維矩陣,其中行與列分別表示邊的起點和終點。頂點上的值存儲在外部。矩陣中可以存儲邊的值。
關聯矩陣:一個二維矩陣,行表示頂點,列表示邊。矩陣中的數值用於標識頂點和邊的關系(是起點、是終點、不在這條邊上等)
常用集合源碼解析
常用集合基本上都是圍繞着數據結構的去實現的(源碼層面解析),必須掌握的ArrayList、LinkedList、HashMap
ArrayList、LinkedList、Vector、HashSet、LinkedHashSet、TreeSet、HashMap、HashTable、TreeMap、LinkedHashMap AbstractQueue、BlockingQueue、Deque最好都學習一些