【Algorithm&DataStructure】極客時間-資料結構與演算法之美專欄筆記I
以下內容均來自本人學習專欄時的個人筆記、總結,侵權即刪
專欄地址:https://time.geekbang.org/column/126
希望看到本文章的,可以去支援一下老師,講的很好!!
目錄
O(n)的排序(非基於比較,對資料要求苛刻,複雜度n-》線性排序)
跳錶(區間查詢)[連結串列中的二分查詢]{Redis-->雜湊表+跳錶}
時間複雜度為O(n)=logn的程式碼
i = 1;
while(n<i){
i = i*2;
}
變數 i 的取值就是一個等比數列。如果我把它一個一個列出來,就應該是這個樣子的:
所以,只需要知道x的值,就可以知道這段程式碼執行的次數了,也就是log2n
而對於 i = i × x 的情況(x是一個常數,可以想成3),也可以得知時間複雜度是logxn
而所有對數階的時間複雜度一般都表示為logn,因為可以通過換底公式,logxn = logx2 × log2n(x是一個常數)
平均時間複雜度=單一情況發生的概率 × 這種情況的時間複雜度
--->>>均攤時間複雜度(思維角度):O(1)->O(1)->O(1)->O(1)->...n次...->O(n) :執行n次O(1)後會有一次O(n)的操作
可以將O(n)分成n次均攤到每個O(1)的操作,這樣算下來整個程式碼的平均時間複雜度也就是O(1)了
沒有頭結點要多判斷什麼?-->哨兵結點作用
/*
* 沒有頭結點的插入、刪除
*/
//一般插入結點
new_node->next = p->next;
p->next = new_node;
//空連結串列第一個結點
if (head == null) {
head = new_node;
}
//刪除結點
p->next = p->next->next;
//空連結串列最後一個結點刪除
if (head->next == null) {
head = null;
}
如果有頭結點(不存資料的結點),不管有沒有結點都可以使用同一個邏輯了,不用再根據特殊情況來判斷
陣列和連結串列的區別
這裡我要特別糾正一個“錯誤”。我在面試的時候,常常會問陣列和連結串列的區別,很多人都回答說,“連結串列適合插入、刪除,時間複雜度 O(1);陣列適合查詢,查詢時間複雜度為 O(1)”。實際上,這種表述是不準確的。陣列是適合查詢操作,但是查詢的時間複雜度並不為 O(1)。即便是排好序的陣列,你用二分查詢,時間複雜度也是 O(logn)。所以,正確的表述應該是,陣列支援隨機訪問,根據下標隨機訪問的時間複雜度為 O(1)。
容器(ArrayList)和陣列的選擇
我個人覺得,ArrayList 最大的優勢就是可以將很多陣列操作的細節封裝起來。比如前面提到的陣列插入、刪除資料時需要移其他資料等。另外,它還有一個優勢,就是支援動態擴容。
- Java ArrayList 無法儲存基本型別,比如 int、long,需要封裝為 Integer、Long 類,而 Autoboxing、Unboxing 則有一定的效能消耗,所以如果特別關注效能,或者希望使用基本型別,就可以選用陣列。
- 如果資料大小事先已知,並且對資料的操作非常簡單,用不到 ArrayList 提供的大部分方法,也可以直接使用陣列。
- 還有一個是我個人的喜好,當要表示多維陣列時,用陣列往往會更加直觀。比如 Object[][] array;而用容器的話則需要這樣定義:ArrayList<ArrayList> array
佇列
阻塞佇列
其實就是在佇列基礎上增加了阻塞操作。簡單來說,就是在佇列為空的時候,從隊頭取資料會被阻塞。因為此時還沒有資料可取,直到佇列中有了資料才能返回;如果佇列已經滿了,那麼插入資料的操作就會被阻塞,直到佇列中有空閒位置後再插入資料,然後再返回。
你應該已經發現了,上述的定義就是一個“生產者 - 消費者模型”!是的,我們可以使用阻塞佇列,輕鬆實現一個“生產者 - 消費者模型”!
併發佇列
前面我們講了阻塞佇列,在多執行緒情況下,會有多個執行緒同時操作佇列,這個時候就會存線上程安全問題,那如何實現一個執行緒安全的佇列呢?
最簡單直接的實現方式是直接在 enqueue()、dequeue() 方法上加鎖,但是鎖粒度大併發度會比較低,同一時刻僅允許一個存或者取操作。實際上,基於陣列的迴圈佇列,利用 CAS 原子操作,可以實現非常高效的併發佇列。這也是迴圈佇列比鏈式佇列應用更加廣泛的原因。在實戰篇講 Disruptor 的時候,我會再詳細講併發佇列的應用。
佇列的應用場景和實現方式選擇
佇列的知識就講完了,我們現在回過來看下開篇的問題。執行緒池沒有空閒執行緒時,新的任務請求執行緒資源時,執行緒池該如何處理?各種處理策略又是如何實現的呢?我們一般有兩種處理策略。
第一種是非阻塞的處理方式,直接拒絕任務請求;另一種是阻塞的處理方式,將請求排隊,等到有空閒執行緒時,取出排隊的請求繼續處理。那如何儲存排隊的請求呢?我們希望公平地處理每個排隊的請求,先進者先服務,所以佇列這種資料結構很適合來儲存排隊請求。我們前面說過,佇列有基於連結串列和基於陣列這兩種實現方式。
這兩種實現方式對於排隊請求又有什麼區別呢?基於連結串列的實現方式,可以實現一個支援無限排隊的無界佇列(unbounded queue),但是可能會導致過多的請求排隊等待,請求處理的響應時間過長。所以,針對響應時間比較敏感的系統,基於連結串列實現的無限排隊的執行緒池是不合適的。而基於陣列實現的有界佇列(bounded queue),佇列的大小有限,所以執行緒池中排隊的請求超過佇列大小時,接下來的請求就會被拒絕,這種方式對響應時間敏感的系統來說,就相對更加合理。不過,設定一個合理的佇列大小,也是非常有講究的。佇列太大導致等待的請求太多,佇列太小會導致無法充分利用系統資源、發揮最大效能。實際上,對於大部分資源有限的場景,當沒有空閒資源時,基本上都可以通過“佇列”這種資料結構來實現請求排隊。
遞迴
遞迴需要滿足的三個條件
- 一個問題的解可以分解為幾個子問題的解
- 這個問題與分解之後的子問題,除了資料規模不同,求解思路完全一樣
- 存在遞迴終止條件
如何編寫遞迴
寫出遞迴公式,找到終止條件,將兩者轉換成程式碼
遞迴注意事項
警惕堆疊溢位(空間複雜度高)
遞迴呼叫一次就會在記憶體棧中儲存一次現場資料,所以在分析遞迴複雜度的時候要考慮這個部分,同時也要考慮遞迴層數過深導致的堆疊溢位。
解決方法:
- 方法①:在比如說遞迴深度超過1000層的時候就丟擲異常,不再進行遞迴(規模小的時候適用,因為實時計算棧的剩餘空間過於複雜)
- 方法②:自己模擬一個棧,用非遞迴程式碼實現
警惕重複計算
排序
O(n^2)的排序(基於比較)
氣泡排序
/**
* 氣泡排序
* @param a
*/
private static void bubbleSortLineryArray(int[] a) {
for(int i=0;i<a.length;i++) {
boolean flag = false; //標記一輪冒泡中是否有交換資料,沒有就直接break
for(int j=0;j<a.length-i-1;j++) {
if(a[j] > a[j+1]) {
int tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;
flag = true;
}
}
if(!flag)break;
}
}
有序度、逆序度
滿有序度:完全有序的陣列的有序度
逆序度的定義正好跟有序度相反(預設從小到大為有序)
插入排序
將資料分為兩個區間:已排序區間和未排序區間
/**
* 插入排序
* @param a
*/
private static void insertSortLineryArray(int[] a) {
for(int i=1;i<a.length;i++) {
int value = a[i];
int j = i - 1;
//查詢插入的位置
for(;j>=0;j--) {
if(a[j] > value) {
a[j+1] = a[j];
}else {
break; //此時a[j+1]就是value要放的地方,a[j+1]的值在上個迴圈已經移動到a[j+2]了
}
}
a[j+1] = value;
}
}
選擇排序
/**
* 選擇排序,每次選擇最小的交換位置
* @param a
*/
private static void selectSortLineryArray(int[] a) {
for(int i=0;i<a.length-1;i++) {
int min = a[i];
int j = i;
int b = i;
for(;j<a.length;j++) {
if(min>a[j]) {
min = a[j];
b = j; //要記錄最小值的位置,然後來交換
}
}
int tmp = a[i];
a[i] = min;
a[b] = tmp;
}
}
O(nlogn)的排序(基於比較)
歸併排序(分治思想=大問題化成小問題)(遞迴程式設計技巧)
如果要排序一個陣列,我們先把陣列從中間分成前後兩部分,然後對前後兩部分分別排序,再將排序好的兩部分合並在一起。就可以得到一個有序的陣列了。
*時間複雜度求解:
我們假設對 n 個元素進行歸併排序需要的時間是 T(n),那分解成兩個子陣列排序的時間都是 T(n/2)。我們知道,merge() 函式合併兩個有序子陣列的時間複雜度是 O(n)。所以,套用前面的公式,歸併排序的時間複雜度的計算公式就是:
T(1) = C; n=1 時,只需要常量級的執行時間,所以表示為 C。
T(n) = 2*T(n/2) + n; n>1
通過這個公式,如何來求解 T(n) 呢?還不夠直觀?那我們再進一步分解一下計算過程。
T(n) = 2*T(n/2) + n
= 2*(2*T(n/4) + n/2) + n = 4*T(n/4) + 2*n
= 4*(2*T(n/8) + n/4) + 2*n = 8*T(n/8) + 3*n
= 8*(2*T(n/16) + n/8) + 3*n = 16*T(n/16) + 4*n
......
= 2^k * T(n/2^k) + k * n
......
通過這樣一步一步分解推導,我們可以得到 T(n) = 2^kT(n/2^k)+kn。
當 T(n/2^k)=T(1) 時,也就是 n/2^k=1,我們得到 k=log2n 。
我們將 k 值代入上面的公式,得到 T(n)=Cn+nlog2n 。如果我們用大 O 標記法來表示的話,T(n) 就等於 O(nlogn)。所以歸併排序的時間複雜度是 O(nlogn)。
快排
快排的思想是這樣的:
- 如果要排序陣列中下標從 p 到 r 之間的一組資料,我們選擇 p 到 r 之間的任意一個資料作為 pivot(分割槽點)。
- 我們遍歷 p 到 r 之間的資料,將小於 pivot 的放到左邊,將大於 pivot 的放到右邊,將 pivot 放到中間。經過這一步驟之後,陣列 p 到 r 之間的資料就被分成了三個部分,前面 p 到 q-1 之間都是小於 pivot 的,中間是 pivot,後面的 q+1 到 r 之間是大於 pivot 的。
- 根據分治、遞迴的處理思想,我們可以用遞迴排序下標從 p 到 q-1 之間的資料和下標從 q+1 到 r 之間的資料,直到區間縮小為 1,就說明所有的資料都有序了。
private static void quickSortLineryArray(int[] a, int start, int end) {
if(a.length == 0 || a.length == 1) {
return ;
}
int i = start;
int j = start; //j負責檢查小於key的
int key = a[end];
while(j < end){
if(a[j] < key) {
int tmp = a[i];
a[i] = a[j];
a[j] = tmp;
i++;
}
j++;
}
int tmp = a[i];
a[i] = a[end];
a[end] = tmp;
if(i > start)quickSortLineryArray(a, start, i-1);
if(i < end)quickSortLineryArray(a, i+1, end);
}
*快排的優化:
快排在最壞的條件下(每次分割槽點都選擇最後一個資料),時間複雜度會退化O(n^2)
所以快排的優化核心就在對於分割槽點的選擇
**常用、簡單的分割槽演算法:
- 三數取中法:從區間的首、尾、中間分別取出一個數,然後比較大小,取這三個數的中間值作為分割槽點。可以根據排序陣列的規模來上升為“五數取中”或者“十數取中”
- 隨機法:隨機選擇一個元素作為分割槽點,看運氣
O(n)的排序(非基於比較,對資料要求苛刻,複雜度n-》線性排序)
桶排序
*資料要求:
- 首先,要排序的資料需要很容易就能劃分成 m 個桶,並且,桶與桶之間有著天然的大小順序。這樣每個桶內的資料都排序完之後,桶與桶之間的資料不需要再進行排序。
- 其次,資料在各個桶之間的分佈是比較均勻的。如果資料經過桶的劃分之後,有些桶裡的資料非常多,有些非常少,很不平均,那桶內資料排序的時間複雜度就不是常量級了。在極端情況下,如果資料都被劃分到一個桶裡,那就退化為 O(nlogn) 的排序演算法了。
*適用場景:
桶排序比較適合用在外部排序中。所謂的外部排序就是資料儲存在外部磁碟中,資料量比較大,記憶體有限,無法將資料全部載入到記憶體中。
計數排序(桶排序的一種特殊情況)
*原理:
①準備:兩個陣列:
- A:存放資料在不同下標(可以看做桶,下標值是資料的值)對應的個數(所謂“計數”)(陣列大小是原陣列資料數值個數)
==轉換成==> 當前桶在已排好序陣列中的位置(下標號),用求和的方式(A[k]儲存小於等於數值k的資料個數)
- B:用於存放已排好序的資料的陣列(同原陣列大小相同)
②移動:每向B新增一個資料,A中對應下標資料上的值就-1(也就是個數少了一個,因為被分配了)
*適用場景&限制:
計數排序只能用在資料範圍(陣列A的大小)不大的場景,如果資料範圍 k 比要排序的資料 n 大很多,就不適合用計數排序了。
而且,計數排序只能給非負整數排序,如果要排序的資料時其他型別的,要將其不改變相對大小的情況下,轉換成非負整數。
基數排序(排序低位-->排序高位)
*資料要求:
資料需要可以分割出獨立的“位”來比較(比如十分位、百分位),而且位之間有遞進的關係(十分位比百分位弱),如果a的資料的高位比b資料大,那剩下的低位就不用比較了。除此之外,每一位的資料範圍不能太大,要可以用線性排序演算法來排序(演算法必須是穩定的,否則低位的排序就沒有意義了),否則基數排序的時間複雜度就無法做到O(n)了。
排序演算法的實現
O(n^2)不一定就是比O(nlogn)執行的時間要長,因為大O時間複雜度一般都省去了某些引數,而這些引數在小規模資料時的作用是不同的。所以對於小規模的資料,有時可以考慮一下複雜度O(n^2)的排序演算法
【Java】Collection.sort()的實現:
二分查詢(O(logn))
著重掌握它的三個容易出錯的地方:
- 迴圈退出條件[ while( low <= high) , <的話可能會錯過最後一次迴圈時low或者high等於value]
- mid的取值
- low 和 high 的更新
實際上,mid=(low+high)/2 這種寫法是有問題的。因為如果 low 和 high 比較大的話,兩者之和就有可能會溢位。改進的方法是將 mid 的計算方式寫成 low+(high-low)/2。更進一步,如果要將效能優化到極致的話,我們可以將這裡的除以 2 操作轉化成位運算 low+((high-low)>>1) [外層括號不能省,優先順序問題]。因為相比除法運算來說,計算機處理位運算要快得多。
那二分查詢能否依賴其他資料結構呢?比如連結串列。答案是不可以的,主要原因是二分查詢演算法需要按照下標隨機訪問元素。我們在陣列和連結串列那兩節講過,陣列按照下標隨機訪問資料的時間複雜度是 O(1),而連結串列隨機訪問的時間複雜度是 O(n)。所以,如果資料使用連結串列儲存,二分查詢的時間複雜就會變得很高。
雖然大部分情況下,用二分查詢可以解決的問題,用雜湊表、二叉樹都可以解決。但是,我們後面會講,不管是雜湊表還是二叉樹,都會需要比較多的額外的記憶體空間。如果用雜湊表或者二叉樹來儲存這 1000 萬的資料,用 100MB 的記憶體肯定是存不下的。而二分查詢底層依賴的是陣列,除了資料本身之外,不需要額外儲存其他資訊,是最省記憶體空間的儲存方式,所以剛好能在限定的記憶體大小下解決這個問題。
二分查詢變體
①查詢第一個值等於給定值的元素
/**
* 查詢第一個值等於給定定值的元素
*/
private static int FirstEqualConst(int[] a, int value) {
int low = 0;
int high = a.length-1;
while(low <= high) {
int mid = low + ((high - low) >> 1);
if(a[mid] > value) {
high = mid - 1;
}else if(a[mid] < value) {
low = mid + 1;
}else {
if((mid == 0) || (a[mid - 1] != value))return mid;
high = mid - 1;
}
}
return -1;
}
②查詢最後一個值等於給定值的元素
/**
* 查詢最後一個值等於給定定值的元素
*/
private static int LastEqualConst(int[] a, int value) {
int low = 0;
int high = a.length - 1;
while(low <= high) {
int mid = low + ((high - low) >> 1);
if(a[mid] > value) {
high = mid - 1;
}else if(a[mid] < value) {
low = mid + 1;
}else {
if(mid == a.length-1 || a[mid + 1] != value)return mid;
low = mid + 1;
}
}
return -1;
}
③查詢第一個大於等於給定值的元素
/**
* 查詢第一個值大於等於給定定值的元素
*/
private static int FirstGreaterEquConst(int[] a, int value) {
int low = 0;
int high = a.length - 1;
while(low <= high) {
int mid = low + ((high - low) >> 1);
if(a[mid] >= value) {
if(mid == 0 || a[mid - 1] < value)return mid;
high = mid - 1;
}else {
low = mid + 1;
}
}
return -1;
}
④查詢最後一個小於等於給定值的元素
/**
* 查詢最後一個小於等於(最靠近)給定定值的元素
*/
private static int LastLessEquConst(int[] a, int value) {
int low = 0;
int high = a.length - 1;
while(low <= high) {
int mid = low + ((high - low) >> 1);
if(a[mid] <= value) {
if(mid == a.length - 1 || a[mid + 1] > value)return mid;
low = mid + 1;
}else {
high = mid - 1;
}
}
return -1;
}
思考題1(LeetCode33)
如果有序陣列是一個迴圈有序陣列,比如 4,5,6,1,2,3。針對這種情況,如何實現一個求“值等於給定值”的二分查詢演算法?
/**
* LeetCode-33-迴圈有序陣列的二分查詢
*/
private static int CircleArray(int[] a, int value) {
/*
* 想法:一次迴圈找到中間點,判斷在哪個區間,複雜度O(n)
*/
int n = a.length;
//flag=0代表value在中間點左邊,flag=1代表value在中間點右邊
int flag = value >= a[0]? 0 : 1;
int max = n - 1 ; //max是陣列最大值下標
int high,low;
for(int i=1;i<n-1;i++) {
if(a[i-1]>a[i]) {
max = i-1;
break;
}
}
if(flag == 0) {
low = 0;
high = max;
while(low <= high){
int mid = low + ((high - low) >> 1);
if(a[mid] > value) {
high = mid - 1;
}else if(a[mid] < value) {
low = mid + 1;
}else {
return mid;
}
}
}else if(flag == 1) {
low = max + 1;
high = n - 1;
while(low <= high){
int mid = low + ((high - low) >> 1);
if(a[mid] > value) {
high = mid - 1;
}else if(a[mid] < value) {
low = mid + 1;
}else {
return mid;
}
}
}
return -1;
}
思考題2(求一個數平方根,小數點精確到後六位)
/**
* 精確到後六位的平方根
*/
private static Double sqrt(double x, double precision) {
double low = 0;
double high = x;
double mid = low + (high - low)/2;
while(high - low > precision) { //precision=0.00001
if(mid * mid > x) {
high = mid;
}else if(mid * mid < x) {
low = mid;
}else {
return mid;
}
mid = low + (high - low)/2;
}
return mid; //取出來的值還要進行額外處理,將六位後的資料消掉
}
跳錶(區間查詢)[連結串列中的二分查詢]{Redis-->雜湊表+跳錶}
|S:O(logn) K:O(n)|
具體實現:建立“索引”,以空間換時間
普通單連結串列查詢一個資料的時間複雜度O(n)
而如果跳錶每兩個結點就抽出一個結點作為上一級索引的結點,第一級索引個數為n/2,第二級索引個數為n/4,排下來第k級索引個數就是n/2^k,而最高階的索引個數有兩個結點,就可以推出級數k=log2n-1,而每一層最多遍歷n+1個結點,所以在跳錶中查詢一個數的時間複雜度就是O(logn)
跳錶索引的動態更新:
插入資料時,如果不更新索引,就可能出現兩個索引結點之間資料非常多的情況,極端一點,可能退化成單連結串列
解決:在插入結點的同時將這個資料插入到部分索引層
為什麼Redis用跳錶不用紅黑樹
Redis中有序集合支援的核心操作:
- 插入一個資料
- 刪除一個資料
- 查詢一個資料
- 按照區間查詢資料
- 迭代輸出有序佇列
其他四個的操作,紅黑樹也可以完成,時間複雜度和跳錶一樣。
但是按照區間查詢資料,跳錶可以做到O(logn)的時間複雜度來定位區間的起點,然後在原始連結串列中往後遍歷就可以了。
其次,跳錶相比紅黑表容易實現+好懂(但是一般程式語言中Map型別都是通過紅黑樹實現)
雜湊表(高效的CRUD)
核心
雜湊函式設計和雜湊衝突解決
雜湊表雜湊衝突的解決方法
開放定址法(不允許在同一個結點)
- 線性探測:衝突後+一個常量,如果為空就插入,如果不為空就再加,一直到尾然後在從頭加
- 二次探測:衝突後+一個常量^2,……
- 雙重雜湊:衝突後換一個雜湊函式計算
連結串列法(允許在同一個結點)
如何設計一個雜湊表
- 一個合適的雜湊函式
- 裝載因子閾值,設計動態擴容策略
過了閾值才擴容:使某次插入時間複雜度上升到O(n),因為要從原來的雜湊表搬移到新的雜湊表
一邊插入一邊搬:每插入一個資料就搬運一個原資料到新雜湊表
- 合適的雜湊衝突解決方法
開放定址法:資料量、裝載因子小(Java中的ThreadLocalMap)
連結串列法:資料量大,存放的物件大(這樣就忽略連結串列中指標的記憶體消耗了)
為什麼雜湊表和連結串列經常一塊使用(順序遍歷)
雜湊表支援非常高效的資料插入、刪除、查詢操作(O(1)),但雜湊表中的資料都是通過雜湊函式打亂之後無規律儲存的。所以如果我們希望按照順序來遍歷資料,就要先取出資料到陣列然後排序,這樣效率就很低。但是如果結合連結串列的話,順序問題就可以通過維護連結串列結點的next指標來實現了。
其中next是指向插入順序上的下一個結點,而hnext是指向雜湊衝突的下一個結點
Java中的LinkedHashMap也是這種結構(Linked並不是指連結串列法解決雜湊衝突,而是雙向連結串列)
雜湊演算法的分散式應用
負載均衡
實現會話粘滯(SessionSticky)[同一個客戶端上,再一次會話中的所有請求都路由到同一個伺服器上]
如果靠維護一張(客戶端ip地址[會話id]+伺服器編號對映)的表來實現的話,如果客戶端很多會浪費記憶體空間。如果客戶端下線、上線、伺服器擴容、縮容都會導致對映失效,維護表成本很高。
我們可以通過雜湊演算法,對客戶端 IP 地址或者會話 ID 計算雜湊值,將取得的雜湊值與伺服器列表的大小進行取模運算,最終得到的值就是應該被路由到的伺服器編號。這樣,我們就可以把同一個 IP 過來的所有請求,都路由到同一個後端伺服器上。
資料分片
將資料分成好幾片存到不同機器
將每個資料中的關鍵字通過雜湊函式計算雜湊值,再跟機器的數量n取模,得到的值就是應存機器的編號(MapReduce基本思想)
分散式儲存
資料分片後,機器數量不夠需求,需要擴容時,所有資料關鍵字就要重新計算雜湊值,然後來搬到新的機器=快取中資料全部失效,客戶端就是請求到資料庫-->雪崩效應。
解決:一開始設計的時候,將所有的快取槽連成一個環(一致性雜湊--環形儲存)[Redis叢集]
https://www.sohu.com/a/158141377_479559
node相當於機器,key所在位置就是算出來的雜湊值。key順時針歸屬於node
為了防止,大部分key歸屬到一個node,提出了“虛擬結點”
簡單來說就是將node拆分成好幾個
相關文章
- 資料結構與演算法之美-王爭-極客時間資料結構演算法
- 陣列(Array)- 極客時間(資料結構與演算法之美)陣列資料結構演算法
- 我的極客時間專欄結課了!!!
- 《資料結構與演算法之美》學習筆記之開篇資料結構演算法筆記
- 《資料結構與演算法之美》學習筆記之複雜度資料結構演算法筆記複雜度
- JavaScript 資料結構與演算法之美 - 時間和空間複雜度JavaScript資料結構演算法複雜度
- 資料結構與演算法之美資料結構演算法
- 資料結構 之 演算法時間複雜度資料結構演算法時間複雜度
- 資料結構與演算法——時間複雜度資料結構演算法時間複雜度
- 《資料結構與演算法之美》資料結構與演算法學習書單 (讀後感)資料結構演算法
- 資料結構與演算法-學習筆記(二)資料結構演算法筆記
- 資料結構與演算法-學習筆記(16)資料結構演算法筆記
- 資料結構與演算法學習筆記01資料結構演算法筆記
- 資料結構與演算法課程筆記(二)資料結構演算法筆記
- 資料結構與演算法之間有何關係?資料結構演算法
- 資料結構與演算法:演算法的時間複雜度資料結構演算法時間複雜度
- 資料結構與演算法之線性結構資料結構演算法
- Nginx學習筆記3--(極客時間-陶輝)Nginx筆記
- 《JavaScript資料結構與演算法》筆記——第3章 棧JavaScript資料結構演算法筆記
- 《JavaScript資料結構與演算法》筆記——第6章 集合JavaScript資料結構演算法筆記
- 大二上 資料結構與演算法筆記 20241024資料結構演算法筆記
- 資料結構與演算法分析學習筆記(四) 棧資料結構演算法筆記
- 演算法與資料結構之集合演算法資料結構
- 資料結構與演算法之排序資料結構演算法排序
- 資料結構與演算法之美-02複雜度分析(下)資料結構演算法複雜度
- 《資料結構與演算法之美》為什麼要學習資料結構和演算法 (讀後感)資料結構演算法
- 菜鳥筆記之資料結構(24)筆記資料結構
- 資料結構筆記資料結構筆記
- 《JavaScript資料結構與演算法》筆記——第4章 佇列JavaScript資料結構演算法筆記佇列
- 《JavaScript資料結構與演算法》筆記——第2章 陣列JavaScript資料結構演算法筆記陣列
- 《資料結構與演算法之美》如何抓住重點,系統高效地學習資料結構與演算法 (讀後感)資料結構演算法
- 資料結構學習筆記-佛洛依德演算法資料結構筆記演算法
- 設計模式之美-王爭-極客時間-返現24元設計模式
- [資料結構與演算法]-動態規劃之揹包演算法終極版資料結構演算法動態規劃
- python之資料結構與演算法分析Python資料結構演算法
- 資料結構與演算法之快速排序資料結構演算法排序
- JavaScript 資料結構與演算法之美 - 十大經典排序演算法JavaScript資料結構演算法排序
- 軟體工程之美-寶玉-極客時間軟體工程