一、為什麼要學習資料結構和演算法
其實,以前我們都會說,學習資料結構有多麼多麼的重要,長篇大論。這次,我們java程式設計師來看看資料結構和演算法重要性。
例題:判斷一個數是否是2的n次方。比如:2,4,8,16是2的n次方;6,10不是。
拿到這道題,用java的思路分析:
2:2
4:2*2
8:2 * 2 * 2
16:2 * 2 * 2 * 2
如果一個數除以2,最後餘數是0,那麼這個數就是2的n次方;如果餘數是1,那麼就不是。程式碼實現如下:
public static void main(String[] args) {
// 隨意給一個數,判斷這個數是不是2的n次方
int n = 16;
int yuShu = n%2;
int chuShu = n/2;
while (chuShu > 1) {
yuShu = chuShu % 2;
chuShu = chuShu / 2;
}
System.out.println( n + " 這個數" + (yuShu == 0?"是":"不是")+"2的n次方");
}
但是,如果使用資料結構呢?下面來分析一下:將所有的十進位制數字都轉成2進位制
2:10
4:100
8:1000
16:10000
他們的特點是什麼呢?第一個數字是1,其餘的都是0
而這幾個數字之前的一個數字是什麼呢?
1:01
3:011
7:0111
15:01111
他們的特點是什麼呢?第一個是0,其餘的都是1.
利用這兩組數字,我們找規律,如果i和i-1按位&與的結果是0,就說明這個i是2的n次方;否則就不是
2 & 1 = 0
4 & 3 = 0
8 & 7 = 0
16 & 15 = 0
但是15 & 14呢,14的二進位制數是01110,01111 & 01110 = 00001
所以,通過對資料的分析,我們可以用一句程式碼判斷
if(n & (n=1) == 0) {
// 這個數就是2的n次方
} else {
// 否則不是
}
二、資料結構概述
資料結構包括:線性結構和非線性結構
1、線性結構
1)線性結構是最常用的資料結構,其特點是資料元素之間存在一對一的線性關係。
2)線性結構有兩種不同的儲存結構,即順序儲存結構和鏈式儲存結構。順序儲存的線性表成為順序表,順序表中的儲存元素是連續的
3)鏈式儲存的線性表成為連結串列,連結串列中的儲存元素不一定是連續的,元素節點中存放資料元素以及相鄰元素的地址資訊。
4)線性結構常見的有:陣列、佇列、連結串列和棧,後面我們會詳細講解
2、非線性結構
非線性結構包括:二維陣列、多維陣列、廣義表、樹結構、圖結構
三、演算法
演算法有五個特徵:有窮性,確定性,可行性,有輸入,有輸出
正確性,可讀性,健壯性,bug(寫出的程式碼bug少,而且系統穩定)
高效率與低儲存:記憶體+CPU 堆疊記憶體 OOM
記憶體佔用最小,CPU佔用最小,運算速度最快。
評價演算法的兩個重要指標:時間複雜度 和 空間複雜度
時間複雜度:執行一個程式所花費的時間。
空間複雜度:執行程式所需要的記憶體。
1、 時間複雜度
1) 計算時間複雜度的意義:分析介面的效能
2) 時間複雜度表示方法:大寫的O(n)表示,全稱是O(nlogn)
3)常見的時間複雜度
計算時間複雜度,通常是計算比較大的,而且是不確定的的數。如果是已經確定的,那麼就不用計算了,常量就是我們說的不用計算的一種。
> 常數:O(1) 1表示的是常數。不是迴圈的次數
比如下面的這個迴圈,時間複雜度是O(1)
public class BigO {
public static void main(String[] args) {
int n = 0; // 這句話的時間複雜度是O(1)
for (int i = 0; i < 3; i++) { // 這句話會執行4次, 它的時間複雜度也是O(1)
n = n + 1; // 這句話會執行3次, 他的時間複雜度也是O(1)
}
}
}
> 對數:O(logn) 或 O(nlogn)
那麼什麼情況下會使用時間複雜度是對數的這種情況呢?來看一下下面的程式碼:
public static void time_log() {
int a = Integer.MAX_VALUE;
int i = 1;
while (i <= a) {
i = i * 2; // 這個迴圈一共要迴圈多少次呢?
// 我們來看看i的值2,4,8,16,32,64,128 2^0,2^1,2^2,2^3,2^4,2^5 ====> 2(n) = a ===> n = log2a
}
}
這裡i = i * 2 這句話需要迴圈多少次呢?其實我們要求的就是:迴圈多少次,i<= a 呢?2^n = a ; 求n=log2a, log以2為底a的對數。
以上是O(logn)的情況,那麼什麼情況下使用O(nlogn)呢?看下面的程式碼:
public static void time_nlogn() {
int a = Integer.MAX_VALUE;
int i = 1;
for (int j = 0; j < 10; j++) {
while (i <= a) {
i = i * 2; // 這個迴圈一共要迴圈多少次呢?
// 我們來看看i的值2,4,8,16,32,64,128 2(1),2(2),2(3),2(4),2(5) 2(n) = a ===> n = log2a
// 外面還有個j,所以就是(j * log2a)次
}
}
}
只有while迴圈的時候,需要執行log2a次。那麼外面多了一層for迴圈,這次要迴圈多少次呢?(j * log2a)次
> 線性:O(n)
線性指的就是O(n),也就是執行n次。
public static void time_on() {
int n = Integer.MAX_VALUE;
int a = 0;
for (int i = 0; i < n; i++) {
a = a + 1; // 這句話的時間複雜度是什麼? O(n) n是幾就執行幾次. n是未知的,不確定的.如果n是確定的,就是常量了. 時間複雜度就是O(1)
}
}
這裡面a = a + 1;這句話會執行多少次呢?他和n有關係,如果n是10就執行10次,n是100就執行100次。有n決定,所以時間複雜度是O(n);
注意:這裡的n是不確定的。如果n是確定的,那麼時間複雜度就是O(1)le
> 線性對數:O(nlogn)
這個在上面已經說過了
> 平方:O(n ^ 2)
/**
* 時間複雜度O(n^2)
*/
public static void time_Onn() {
int n = Integer.MAX_VALUE;
int a = 0;
for(int i = 0; i < n; i++) {
for (int j = 0; j< n; j++) {
a = a + 1; // 這句話執行多少次呢?也就是說它的時間複雜度是多少呢? 外層迴圈執行n次,內層迴圈也是n次,所以最終執行n*n次,所以時間複雜度是O(n^2)
}
}
}
這裡有兩層迴圈,外層迴圈執行n次,記憶體迴圈也是n次,所以程式碼a = a + 1;執行O(n*n)次。
常見的O(n^2)還有氣泡排序
public static void time_Onn2() {
int[] num = {3,6,1,0,8,5};
int n = num.length;
int a = 0;
for(int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
a = a + 1; // 這句話執行多少次呢?也就是說它的時間複雜度是多少呢? 來找找它的規律
/*
* 外層迴圈i=n, 內層程式碼執行1次
* 外層迴圈i=n-1,內層程式碼執行2次
* 外層迴圈i=n-2,內層程式碼執行3次
* 外層迴圈i=n-3,內層程式碼執行4次
* 外層迴圈i=1,內層程式碼執行n次
*
* 一共執行多少次呢? 0+1+2+3+......+n = n * (n+1)/2次
* 也就是冒泡演算法的時間複雜度是O(n*(n+1)/2)次
*/
}
}
}
氣泡排序的時間複雜度是多少呢?我們來分析一下:
外層迴圈i=n, 內層程式碼執行1次
外層迴圈i=n-1,內層程式碼執行2次
外層迴圈i=n-2,內層程式碼執行3次
外層迴圈i=n-3,內層程式碼執行4次
外層迴圈i=1,內層程式碼執行n次
一共執行多少次呢? 0+1+2+3+......+n = n * (n+1)/2次
也就是冒泡演算法的時間複雜度是O(n*(n+1)/2)次。
在計算時間複雜度的時候去掉常數,所以就是O(n^2)
> N次方:
如果是三層迴圈,四層迴圈呢?那就是 n * n * n * n=n^4
上面的冒泡演算法會執行n*(n+1)/2 = (n^2 + n)/2 ===>當有加減法的時候,這個時間複雜度怎麼計算呢?
取最大的就可以,這個最大是:首先去掉常數後,n2比n的階層高,所以最後是O(n2)
4) 怎麼計算時間複雜度?
第一步:找有迴圈的地方
第二步:找有網路請求的地方,包括RPC協議請求,資料庫請求
網路請求可以通過列印日誌來計算時間。
5) 常見時間複雜度的執行效率
O(1) > O(logn) > O(n) > O(nlogn) > O(n^2) > O(n^x)
O(1)的執行效率是最高的。
所以,我們在優化的時候,目標是將所有的時間複雜度往O(1)進行優化。
實際上,O(1) > O(logn) > O(n) > O(nlogn) 效果都是很好的,幾乎優化的空間不是很大。我們的最終目標就是將O(n^2) 和 O(n^x)往前優化。
- 案例1:
再來看看最開始的這個案例:判斷一個數是否是2的n次方。比如:2,4,8,16是2的n次方;6,10不是。
如果一個數除以2,最後餘數是0,那麼這個數就是2的n次方;如果餘數是1,那麼就不是。程式碼實現如下:
public static void main(String[] args) {
// 隨意給一個數,判斷這個數是不是2的n次方
int n = 16;
int yuShu = n%2;
int chuShu = n/2;
while (chuShu > 1) {
yuShu = chuShu % 2;
chuShu = chuShu / 2;
}
System.out.println( n + " 這個數" + (yuShu == 0?"是":"不是")+"2的n次方");
}
那麼這段程式碼的時間複雜度是多少呢?log2n:log以2位底n的對數。O(logn)
後來通過二進位制程式碼進行優化,優化後的結果是
if(n & (n=1) == 0) {
// 這個數就是2的n次方
} else {
// 否則不是
}
這段程式碼的時間複雜度是O(1)。
我們優化以後,將O(logn)優化為O(1)了,效率大大提高了
- 案例2
比如,我們在看一段程式碼的時候,發現有效能瓶頸,然後這段程式碼使用的排序方式是氣泡排序,如何對這個排序進行優化呢?
找更優秀的替代排序方法,快速排序,歸併排序,堆排序等。
2、空間複雜度
1)空間複雜度分析的意義
找出哪些地方花費記憶體多,哪些資料佔用的記憶體開銷大
2)如何找出程式的空間複雜度?
開空間的地方,比如:陣列,連結串列,快取Map,Set,佇列Queue,遞迴等
資料結構和演算法書籍推薦