因为之前的笔记和书籍相关知识都是零零散散的, 没有一个汇总, 所以写了这篇博客。有些算法很简单,复杂度一眼都能看得出来, 几乎不需要记忆 , 但是有些算法或者数据结构的操作的复杂度就不是一眼可以看得出来, 推导也是很费时间的, 所谓常识就是应该熟记于心且被认可的知识。
注:以下所有代码皆可以直接运行, 都已经测试过。
必须掌握的知识
常用算法的复杂度
冒泡排序
想象就是很多泡泡,最大的泡泡每次浮到那个数组最后面
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;
}