以整数数组递增排序为例:
1. 选择排序 selection sort
从第一个元素开始,在全数组里寻找最小的数,和第一个元素交换;
前进至第二个元素,在第二个元素为起始的余下数组中寻找最小的数,和第二个元素交换;
可以递归实现:
SelectionSortRecursion( data, start )
{
if( start < data.length - 1 )
{
swap( data, start, findMinimumIndex(data, start) );
selectionSortRecursion( data, start+1 );
}
}
也可循坏实现。
(n-1)+(n-2)+ . . . + 1 =》n( n -1 )/2 => O(n^2) in best, average, worst cases;
特点:数组初始顺序对于比较次数没有影响。选择排序是in-place排序,即不需辅助存储空间。通常的选择排序实现都是非稳定的(not stable),即值相同的元素的初始顺序可能会被改变。
优点:最多只需n-1次交换操作,在移动数组元素需要很大代价,而比较代价很小的情况下,选择排序可能会比其他排序算法表现更好。
2. 插入排序 Insersion sort
第二个元素和第一个元素比较,若小于第一个元素,插入至第一个元素前面,若大,不变换两个元素位置。得到排好序的两个元素。第三个元素和前两个元素比较,插入到正确的位置,以此类推,直至最后一个元素。
InserstionSort( data )
{
for( pos = 1; pos < data.length; pos++)
{
current = data[pos];
for( i = 0; i < pos; i++ )
{
if( data[i] > current)
{
//将从 i 开始的 current - i 个元素向数组后方移动一个位置
arrayCopy( data, i, data, i+1, pos - i);
data[i] = current;
break;
}
}
}
}
best-case running time is O(n) when the original array is sorted。这意味着插入排序对于在已排序数组中插入新元素并排序效果较好。然而,插入排序的average和worst均是O(n^2)这意味着当n比较大且初始数组为随机无序状态时,插入排序的性能不能保证。
特点:插入排序是稳定的, in-place排序算法,对于排序小规模数组效果较好。插入排序经常被当做子模块(building block)用于构建更复杂的排序算法。
3. 快速排序
快速排序属于divide-and-conquer算法范畴,基本流程是:
选择pivot(可以随机也可以指定某一固定位置)-> 根据数组元素与pivot的值的比较(大于等于 或 小于),将数组分为左右两部分。
递归上述流程直到只剩一个元素无法分割。
Literate Programming 上的一种实现:
struct pivot_random {
pivot_random() {
srand(static_cast<unsigned int>(time(0)));
}
int operator() (int low, int high)
{
return low + (rand()%(high-low));
}
};
int Partition(int* input, int low, int high, int pivot)
{
swap(input[low], input[pivot]);
pivot = low;
while(low <= high)
{
while(input[high] >= input[pivot] && high >= low)
high--;
if( high < low )
break;
else {
swap(input[high], input[pivot]);
pivot = high--;
}
while(input[low] <= input[pivot] && high >= low)
low++;
if( low > high )
break;
else {
swap(input[low], input[pivot]);
pivot= low++;
}
}
return pivot;
}
void QuickSort(int* input, int low, int high, pivot_random pf)
{
if(high > low)
{
int pivot = pf(low, high);
pivot = Partition(input, low, high, pivot);
QuickSort(input, low, pivot-1, pf);
QuickSort(input, pivot+1, high, pf);
}
return;
}
最坏情况O(n^2),最优情况O(nlog(n)),平均O(nlog(n)),大多数的快排实现不稳定(not stable)。
4. 归并排序 Merge sort
归并排序也是divide-and-conquer分治算法的一种。
void Merge(int* input, int low, int high)
{
int* temp = new int[high-low+1];
int start1 = low, end1 = (low+high)/2, start2 = (low+high)/2+1, end2 = high;
int i;
for(i = 0; start1 <= end1 && start2 <= end2; i++)
{
if(input[start1] <= input[start2])
temp[i] = input[start1++];
else
temp[i] = input[start2++];
}
while(start1 <= end1)
temp[i++] = input[start1++];
while(start2 <= end2)
temp[i++] = input[start2++];
i--;
while( i>= 0 )
{
input[i+low] = temp[i];
i--;
}
}
void MergeSort(int* input, int low, int high)
{
if(low < high)
{
MergeSort(input, low, (low+high)/2);
MergeSort(input, (low+high)/2+1, high);
Merge(input, low, high);
}
}
归并排序在划分子数组时,若子数组的大小小于一定值时,如10,可以直接调用其他排序算法,如插入排序:
if( high - low < 10 )
{
InsertionSort(data, low, high);
return data;
}
这是对归并排序常用的优化方法,因为插入排序的overhead比归并排序要小,而且插入排序在小规模数组上效果较好。
归并排序对于数据量大到无法完全装入内存的数组是个很好的选择。在通常情况下,将大文件分割成许多小文件,每一个小文件都可以装入内存,排好序后再写回文件,然后调用Merge流程,输入来自各个排好序的小文件的一部分,输出直接写到最终的文件。
归并排序的best,average,worst均是O(nlog(n)),可以保证性能的上界,但是归并排序需要额外的O(n)的存储空间,远大于其他排序算法堆空间的要求。通常的归并排序算法实现是stable的但不是in-place的。