因爲之前的筆記和書籍相關知識都是零零散散的, 沒有一個彙總, 所以寫了這篇博客。有些算法很簡單,複雜度一眼都能看得出來, 幾乎不需要記憶 , 但是有些算法或者數據結構的操作的複雜度就不是一眼可以看得出來, 推導也是很費時間的, 所謂常識就是應該熟記於心且被認可的知識。
注:以下所有代碼皆可以直接運行, 都已經測試過。
必須掌握的知識
常用算法的複雜度
冒泡排序
想象就是很多泡泡,最大的泡泡每次浮到那個數組最後面
void bubble_sort(int a[], int n)
{
int i, j, temp;
for (j = 0; j < n - 1; j++)
for (i = 0; i < n - 1 - j; i++)
{
if(a[i] > a[i + 1])
{
temp = a[i];
a[i] = a[i + 1];
a[i + 1] = temp;
}
}
}
插入排序
想象手上有幾張牌, 現在你抽了一張牌, 然後需要從手上最右邊的牌開始比較,然後插入到相應位置
void insertion_sort(int test_array[], size_t length)
{
int i = 0, key = 0;
for (size_t index = 1; index < length; ++index)
{
i = index - 1, key = test_array[index];
while (i >= 0 && key < test_array[i])
{
test_array[i + 1] = test_array[i];
i = i - 1;
}
test_array[i + 1] = key;
}
for (size_t ii = 0; ii < length; ++ii, ++test_array)
{
cout << *test_array << endl;
}
}
歸併排序
歸併排序用了分治的思想,有很多算法在結構上是遞歸的:爲了解決一個給定的問題,算法要一次或多次地遞歸調用其自身來解決相關的子問題。這些算法通常採用分治策略(divide-and-conquier):將原問題劃分成n個規模較小而結構與原問題相似的子問題;遞歸地解決這些子問題,然後再合併其結果,就得到原問題的解。
分治模式在每一層遞歸上都有三個步驟:
分解(divide):將原問題分解成一系列子問題;
解決(conquer):遞歸地解各子問題。若子問題足夠小,則直接求解;
合併:將子問題的結果合併成原問題的解。
下面是一個比較直白明瞭的歸併c++實現(其實可以寫成不用動態分配內存的,但是這裏爲了直白起見):
/*
* p: 左數組第一個元素下標
* q: 左數組最後一個元素下標
* r: 右數組最後一個元素下標
*/
void merge(int *array, int p, int q, int r)
{
int n1, n2, i, j, k;
int *left=NULL, *right=NULL;
n1 = q-p+1;
n2 = r-q;
left = (int *)malloc(sizeof(int)*(n1));
right = (int *)malloc(sizeof(int)*(n2));
for(i=0; i<n1; i++)
{
left[i] = array[p+i];
}
for(j=0; j<n2; j++)
{
right[j] = array[q+1+j];
}
i = j = 0;
k = p;
while(i<n1 && j<n2)
{
if(left[i] <= right[j])
{
array[k++] = left[i++];
}
else
{
array[k++] = right[j++];
}
}
for(; i<n1; i++)
{
array[k++] = left[i];
}
for(; j<n2; j++)
{
array[k++] = right[j];
}
free(left);
free(right);
left = NULL;
right = NULL;
}
void merge_sort(int *array, int p, int r)
{
int q;
if(p < r)
{
q = (int)((p+r)/2);
merge_sort(array, p, q);
merge_sort(array, q+1, r);
merge(array, p, q, r);
}
}
快速排序
與歸併排序一樣, 快排也是用了分治的思想。
你可以想象一個兩副牌然後隨意取出一張牌pivot,其他的所有牌都跟這張pivot牌比較, 大的放右邊那一摞A,小的放左邊B。
接着再從左邊這一摞B再隨意取出一張牌pivot,其他的所有牌都跟這張pivot牌比較, 大的放右邊那一摞,小的放左邊,遞歸下去。
A也重複上述步驟遞歸。
遞歸結束之後, 左邊的都比右邊的小, 而且是有序的。
void swap(int *a, int *b)
{
int temp = 0;
temp = *a;
*a = *b;
*b = temp;
}
int partition(int *array, int p, int r)
{
int i = 0, j = 0, pivot = 0;
pivot = array[r];
i = p-1;
for(j=p; j<=r-1; j++)
{
if(array[j] <= pivot)
{
i++;
swap(&array[i], &array[j]);
}
}
swap(&array[i+1], &array[r]);
return i+1;
}
/*
通常,我們可以向一個算法中加入隨機化成分,以便對於所有輸入,它均能獲得較好的平均情況性能。將這種方法用於快速排序時,不是始終採用A[r]作爲主元,而是從子數組A[p..r]中隨機選擇一個元素,即將A[r]與從A[p..r]中隨機選出的一個元素交換。
*/
int rand_patition(int test_arr[], int p, int r)
{
srand(static_cast<unsigned>(time(nullptr)));
int rand_index = (rand() % (r - p) ) + p + 1;
swap(&test_arr[rand_index], &test_arr[r]);
return partition(test_arr, p, r);
}
void quick_sort(int *array, int p, int r)
{
int q = 0;
if(p < r)
{
q = rand_patition(array, p, r);
quick_sort(array, p, q-1);
quick_sort(array, q+1, r);
}
}
快速排序思想的應用
問題 : 查找數組中第k大的數字
算法思想 : 因爲快排每次將數組劃分爲兩組加一個樞紐元素,每一趟劃分你只需要將k與樞紐元素的下標進行比較,如果比樞紐元素下標大就從右邊的子數組中找,如果比樞紐元素下標小從左邊的子數組中找,如果一樣則就是樞紐元素,找到,如果需要從左邊或者右邊的子數組中再查找的話,只需要遞歸一邊查找即可,無需像快排一樣兩邊都需要遞歸,所以複雜度必然降低。
二分查找
二分查找的複雜度計算方法:
時間複雜度可以視爲while循環的次數。
總共有n個元素,
漸漸跟下去就是n,n/2,n/4,….n/2^k(接下來操作元素的剩餘個數),其中k就是循環的次數
由於你n/2^k取整後>=1(接下來操作元素的剩餘個數至少爲一個)
即令n/2^k=1
可得k=log2n,(是以2爲底,n的對數)
所以時間複雜度可以表示O(h)=O(log2n)
遞歸版本:
int binary_search(int arr[], int low, int high, int key)
{
if ( low <= high)
{
int mid = (low + high) / 2;
if ( key == arr[mid] )
return mid;
else if ( key < arr[mid])
binary_search(arr, low, mid - 1, key);
else
binary_search(arr, mid + 1, high, key);
}
else
return -1;
}
非遞歸版本:
int non_recursion_bs(int arr[], int low, int high, int key)
{
int mid = 0;
while (low <= high)
{
mid = ( low + high ) / 2;
if ( key == arr[mid] )
return mid;
else if ( key < arr[mid] )
high = mid - 1;
else
low = mid + 1;
}
return -1;
}