是否拥有扎实的算法知识和技术基础,是区分真正熟练的程序员与新手的一项重要特征。虽然利用当代的计算技术,无需了解很多算法方面的知识,也可以完成一些任务,但是如果有良好的算法基础和背景的话可以做更多的事情,并且拥有更强的思维能力。学生时期虽然全面学习过算法导论里的相关知识,但是学生时没有太多的编程经验,实践算法时候需要边看书上的伪代码边写程序才能实现,经过工作几年,很多知识已经不记得了。现在想重新拿起,因此将实践过程做些记录方便加深理解与记忆。
根据二者排序算法的时间复杂度与实践中对同样规模问题计算所花费的时间可以看到,随着问题规模的增长,归并排序比插入排序有非常明显的优势,对于规模很大的问题,一台性能非常良好的计算机A使用插入排序可能要花费好几个小时的时间,而一台性能普通的计算机B使用归并排序则只需花费几秒的计算时间,差距就是这么明显。因此对于大规模的问题,算法的设计至关重要。
插入排序
以我们玩扑克牌时摸牌过程可以非常形象地理解插入排序,我们每从牌堆里摸起一张牌,边从右往左比较每张排的大小,然后将当前的牌插入到第一张比它小的牌后面,这样的排序过程保证手里的牌总是排好序。
下图是书中形象地介绍插入排序实现过程:
插入排序过程从第2个数开始遍历输入数组,图中深黑色部分为每趟排序过程的“当前牌”,将“当前牌”逐个与前面灰色元素的数值比较,如果该数值比“当前牌大”,则将该元素往后移动一个位置,直到找到第一个比“当前牌”小的元素,然后将“当前牌”插入到该元素的后面。
插入排序C++实现:
插入排序最坏情况是输入数据为逆序的时候,这时候每趟排序,“当前牌”都需要与前面所有的元素进行比较,时间复杂度为:
我实践中对100万个逆序的输入数据进行排序,需要时间为1143.4秒
#include <sys/time.h>
#include <iostream>
void InsertionSort(double* data, const int len)
{
assert( data != NULL );
double key = 0.0;
for( int j = 1; j < len; ++j)
{
key = data[ j ];
int i = j - 1;
while ( (i >= 0) && ( data [ i ] > key ) )
{
data[ i + 1 ] = data[ i ];
--i;
}
data[ i + 1 ] = key;
}
}
int main()
{
const int len = 1000000;
double data[ len ] = {0.0};
for( int i = len, j = 0; i > 0 && j < len; --i, ++j){
data[ j ] = i;
}
struct timeval time_start, time_end;
gettimeofday(&time_start, NULL);
//1000000 Time : 1143.4 (s)
InsertionSort( data , len);
gettimeofday(&time_end, NULL);
double time = 1000000 *(time_end.tv_sec -time_start.tv_sec) +
(time_end.tv_usec -time_start.tv_usec);
time /= 1000000;
std::cout<<"Time : "<<time<<" (s) "<<std::endl;
return 0;
}
归并排序
归并排序是一种分治策略:将原问题划分为n个规模更小而结构与原问题相似的子问题,递归地解决这些子问题,然后再合并其结果,得到原问题的解。
分治模式的三个步骤:
分解:将原问题分解为一系列子问题。
解决:递归地解决各子问题,若子问题足够小,则直接求解。
合并:将子问题的结果合并成原问题的解。
对于归并排序来说:
分解:将n个元素分成各n/2个元素的子序列
解决:用归并排序对两个子序列递归地排序
合并:合并两个已排序的子序列以得到排序结果。
形象地理解合并过程:两堆已排好序的扑克牌,最小的牌放在牌堆的最上面,每次比较两堆牌最上面的两张牌,取出面值小的牌插入手中,这样每次取出牌后牌堆顶部的牌都分别是两堆牌中最小的牌,而手上的牌是排好序的,当其中一堆牌全部取出后将另一堆所有的牌一次取出放入手中即可。
归并排序C++实现:
归并排序时间复杂度为:
实践中同样对100万个逆序的输入数据进行排序,需要时间为0.13759秒
#include <sys/time.h>
#include <iostream>
///合并过程:输入数组p~q和q+1~r的元素分别是已排好序的两个子序列
void Merge(double *data, int p, int q, int r)
{
assert( data != NULL );
const int n1 = q-p+1;
const int n2 = r-q;
///已排好序的两个子数组
double* L = new double[ n1 ];//左序列
double* R = new double[ n2 ];//右序列
for( int i = 0; i < n1; ++i){
L[i] = data[ p + i ];
}
for( int i = 0; i < n2; ++i){
R[i] = data[ q + i + 1];
}
int i = 0;
int j = 0;
///将输入数组中p~r元素合并为已排序的序列
for( int k = p; k <= r; ++k)
{
//若左序列已经全部放入数组中,
//则将右序列剩下的元素依次放入数组中完成排序过程
if( (i == n1) && ( j < n2 ) )
{
for( i = j ; i < n2; ++i)
{
data[ k++ ] = R[ i ];
}
}
else
//若右序列已经全部放入数组中,
//则将左序列剩下的元素依次放入数组中完成排序过程
if( ( j == n2) && ( i < n1 ) )
{
for( j = i ; j < n1; ++j)
{
data[ k++ ] = L[ j ];
}
}
else
if( ( i < n1) && ( j < n2 ) )
{
///依次比较左右子序列最小的元素,将最小值放入到排序数组中,
///然后相应的子序列位置标志量向右移动一个位置。
if( L[i] <= R[j] )
{
data[ k ] = L[i];
++i;
}
else
{
data[ k ] = R[ j ];
++j;
}
}
}
delete []L;
delete []R;
}
void MergeSort(double *data, int p, int r)
{
assert( data != NULL );
if( p < r)
{
int q = (p+r)/2;
MergeSort(data, p, q);
MergeSort(data, q+1, r);
Merge(data, p, q, r);
}
}
int main()
{
const int len = 1000000;
double data[ len ] = {0.0};
for( int i = len, j = 0; i > 0 && j < len; --i, ++j){
data[ j ] = i;
}
struct timeval time_start, time_end;
gettimeofday(&time_start, NULL);
//1000000 Time : 0.13759 (s)
MergeSort(data, 0, len - 1);
gettimeofday(&time_end, NULL);
double time = 1000000 *(time_end.tv_sec -time_start.tv_sec) +
(time_end.tv_usec -time_start.tv_usec);
time /= 1000000;
std::cout<<"Time : "<<time<<" (s) "<<std::endl;
return 0;
}
下图帮助理解归并排序分解合并过程:
原问题是对数组A中的所有元素的进行由小到大排序,调用归并函数是输入参数p1=1, r1 = 10,然后将输入序列从中间q=(p1+r1)/2处分为左序列p2 = 1, r2 = 5, p3 = 6, r3 = 10, 以此类推递归地对子序列进行排序,直到子序列为单个元素时则为已排好序的序列,然后从下往上进行合并,最终得到排好序的数组。