《我的第一本演算法書》筆記一

Orige發表於2020-11-14

一、資料結構

資料儲存在記憶體中的順序和位置關係 就叫資料結構

常見的幾種資料結構

1、陣列和連結串列

陣列記憶體連續 易讀難增刪

陣列空間連續 訪問直接索引訪問 而增刪則需要移除資料後將該元素後面所有的元素往前移保持記憶體連續 因此增刪難

後者記憶體不連續 分散儲存 易增刪難讀

連結串列讀取必須從表頭讀起(雙向連結串列也可以從隊尾) 一直找到自己想要讀取的資料為止 因此讀取難 而一旦找到了自己想要的資料 增刪只需要更改前後兩個資料的next指標即可 因此增刪易
連結串列擴充有雙向 迴圈 雙向迴圈 只是指標的增加意味著記憶體空間的更多需求和更多指標的更改

2、棧和佇列 均無法訪問中間資料

棧先進後出 訪問中間資料必須將資料出棧移至棧頂 訪問只限棧頂
佇列先進先出 訪問中間資料必須將資料出隊移至對頭 訪問時可以訪問隊頭和隊尾

3、雜湊表

記憶體是不連續的
對於我們想要修改的資料 我們訪問需要通過雜湊函式(常見取模)訪問到資料
發生鍵衝突也就是雜湊衝突的時候 我們會通過相對應的方法將雜湊值儲存(鏈地址是同鍵存為一個連結串列,開放地址法是候補雜湊函式,等)

因此我們訪問資料和修改資料是很簡單的

4、堆和二叉樹

堆是一種圖的樹形結構 被用於實現“priority queues” (取值必須從最小值按順序取出)

堆有一個性質:子節點的大小 必須 大於父節點 所以無論資料量有多少 我們取出最小值的時間複雜度都是最小的 o(1) 但是由於性質的存在 取出最小值之後 需要將最後的資料移到最頂端 並重新比較整理 因此時間複雜度是logn

二叉樹
二叉查詢樹 也是一種圖的資料結構

二叉樹有兩個性質:
第一個是每個結點的值都大於其左子樹上任意一個結點的值;
第二個是每個結點的值都小於其右子樹上任意一個結點的值;
如果理解了這兩個性質 我們就能得出結論最小值肯定是從頂端開始往左邊一直找找到左邊的末端,而最大值要從頂端往右邊一直找找到右邊的末端
增加刪除和查詢結點的時候 只要考慮到值和結點的大小比較 大於就往右邊放 小於就往左邊放
而時間複雜度則是需要考慮到二叉樹的高度的 如果樹朝單側發展 那麼查詢到的肯定就是o(n) 如果是比較均衡的樹形結構 那麼肯定是o(logn)

二、排序

關於排序有兩個概念
第一個叫穩定排序和不穩定排序 意思是兩個相同的元素在排序前後兩個元素的順序並沒有因為排序而發生變動 就叫穩定排序 反之不穩定
第二個叫時間複雜度 也是時間複雜性 是衡量一個排序演算法的最直觀的指標

1、氣泡排序

從左到右慢慢挨個比較

時間複雜度n2 穩定排序

虛擬碼:
for (i = 0;i<length-1;i++)
{
for (j = 0;j<length-1-i;j++)
{
if array[j] > array[j+1]
{
temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
}

2、選擇排序

選擇最小的 交換左邊第一個 然後第二輪從第二個開始選出最小的 交換第二個 依次進行

時間複雜度n2 不穩定排序
這邊重點提一句 因為關於選擇排序是不是穩定排序 我覺得是需要考慮排序的演算法規則的 首先我們在未排序部分選擇一個最小元素,如果我們將它直接移動到未排序部分之前 那麼這個演算法是穩定的,而如果我們將該元素和未排序部分的第一位進行交換那麼肯定是不穩定的排序,至於為什麼大家都考慮為不穩定排序的原因很大一部分原因是交換的代價遠遠小於移動的代價!

虛擬碼:
for i=0;i<length-1;i++
{
int minIndex = i;
for j=i+1;j<length;j++
{
if array[j] < array[minIndex]
minIndex = j;
}
temp = array[i];
array[i] = array[minIndex];
array[minIndex] = temp;
}

3、插入排序

從右側未排序區域取一個元素出來 和左側已排序區域比較 插入到合適的位置 就叫插入排序

時間複雜度n2 穩定排序

關於插入排序 有兩種極端情況 如果剛好是排好序的且是和要求的一樣正序的話 那麼時間複雜度是n,如果是排好序且是反序的話 則是n2 所以平均是n2
虛擬碼:
int i, j;
for (i = 1; i < length; i++)
{
cur = num[i];
for (j = i - 1; j >= 0 && num[j] > cur; j--)
{
num[j + 1] = num[j];
}
num[j + 1] = cur;
}

4、堆排序

堆排序的特點 其實就是利用了堆的資料結構特點 首先取出根結點 然後重構堆,再取出根結點放在後面,再重構,直到取完該堆資料則排序完成。

時間複雜度 nlogn 不穩定排序

圖解堆排序

5、歸併排序

把一個有序的序列分成長度相同的兩個子序列 然後遞迴將子序列分成兩個相同長度的更小子序列 直到子序列無法繼續往下分(也就是隻有一個元素的時候) 然後就開始對子序列開始進行歸併,而歸併指的就是兩個排好序的子序列合成一個有序序列,每次依次比較每個子序列的的第一位元素的大小 然後依次取出歸併成一個父序列 直到所有的子序列歸併為一個序列為止

時間複雜度 nlogn 穩定排序

虛擬碼:主要是遞迴遍歷排序
public static void Sort(int[] a, int f, int e)
{
if (f < e)
{
int mid = (f + e) / 2;
Sort(a, f, mid);
Sort(a, mid + 1, e);
MergeMethid(a, f, mid, e);
}
}
private static void MergeMethid(int[] a, int f, int mid, int e)
{
int[] t = new int[e - f + 1];
int m = f, n = mid + 1, k = 0;
while(n <= e && m <= mid)
{
if (a[m] > a[n]) t[k++] = a[n++];
else t[k++] = a[m++];
}
while (n < e + 1) t[k++] = a[n++];
while (m < mid + 1) t[k++] = a[m++];
for (k = 0, m = f; m < e + 1; k++, m++) a[m] = t[k];
}

6、快速排序

快排運用了二分的思想,首先選擇一個基準,定義左右兩端指標,先從左到右進行掃描直到,R[hi] < temp,將R[hi]移動至lo所在位置 [公式] 從右往左進行掃描,直到R[lo] > temp,將R[lo]移動到hi所在位置上,左右端指標在排序過程中從陣列的兩端往中間進行靠近,直到hi == lo。而快速排序則要進行多次快排過程,直到劃分的區間最後長度僅為1.
快速排序的優異之處 在於它本身會對所有的值和基準值進行比較 而這個比較 將會在整個排序週期中 只有1次 如果a<b 那麼a和b將永遠不會再進行比較 如果a<b<c 同樣的 那麼a將永遠不會和c進行比較 就不會有任何的冗餘操作

時間複雜度 nlogn 不穩定排序

虛擬碼:
void QuickSort(int R[], int lo, int hi){
int i = lo, j = hi;
int temp;
if(i < j){
temp = R[i];
while (i != j)
{
while(j > i && R[j] >= temp)-- j;
R[i] = R[j];
while(i < j && R[i] <= temp)++ i;
R[j] = R[i];
}
R[i] = temp;
QuickSort(R, lo, i - 1);
QuickSort(R, i + 1, hi);
}
}

三、陣列的查詢

1、線性查詢

2、二分查詢

通常是用於已經排好序的查詢演算法

相關文章