排序的广泛使用使得研究排序算法成为算法的入门案例,本文主要罗列比较排序以及非比较排序(带限制的数据)的常用方法(默认从小到大)。
1, bubble,“冒泡”还是蛮形象的,每一轮中最终获得了当前数组中的最大值,而最大值的获取就是通过与邻近元素的比较逐步上升的。如果你对算法不了解,让你自己去排序一组数,那最直观的就是我每次拿到最大数保存,然后从余下的数中再递归找最大,所以bubble类似于select, 每一次都是获得极值,故两者复杂度的常数因子也是相差无几,O(1+2+3+…+n-1) = O(n^2), 然后为了更方便的code,第一轮比较后最大的数字就得到了,而它是放置于a[n-1]的,以此类推,外层循环 i=n-1 down to 0,每一次循环相当于排好一个位置,第二层就是为了得到它的比较次数,换言之就是从第0个元素访问到i-1个(当然你硬要比较到n个也无所谓),j=0 to i-1。
2,select,每轮中找到待排元素的最小值(注意是“找到”),然后与当前头元素交换(很容易只将最小值覆盖头元素,终归是没有理解透彻),复杂度同bubble。
3, insert, 玩扑克牌,嗯,就是这样!每轮中选取一个要插入的元素,寻找插入的位置,区别在于如果当前元素大,则将其后移一位,减少了swap的次数,所以相比bubble & select, insert很好的利用了”已排好元素“这个信息,每轮中无需比较前面的所有元素,故常数因子小于前两者(由于常数因子较小,实现简单,故在小规模排序中应用广泛)。
4,merge, 比较排序的优化无非就是减少不必要的比较(这一点insert已经有些苗头了,很好的利用了已排序数据的信息),divide and conquer,优势在于一半的元素无需和额外的另一半比较了,当然代价就是需要做一次归并,而且归并操作需要O(n)的空间,但我们发现在子问题分解过程中引入的新问题可以在较低的复杂度内解决,这就是divide and conquer的核心,至于为什么划分后引入的子问题复杂度较低?这个背后有什么东西在支撑呢?(我唯一能想到的就是“这是个哲学问题”)当然因为涉及到子问题的划分(递归),涉及到栈的使用加之merge中需要额外内存,所以对于小规模数据insert有更好的性能。
5, heap,万变不离其宗,有点类似merge, 通过类似二叉树的heap只比较一半的元素,利用了data structure来提升,本质上就是一棵二叉树,只不过通过数学关系简化了二叉树的节点,left = 2*i, right = left+1(注意这里是从1 to n,访问元素时记得减1),我们来倒推,堆已建好,显然只需每次取出最大值,swap(a[1-1],a[heap_size-1]), 然后max_heapify, 这里的关键就是max_heapify针对头可能有问题,而left and right是堆,显然需要层层沉降来维护。最后就是build_heap,这里很好的利用max_heapify, 而后者的要求是left and right are heap, 所以需要从最后的子元素开始,i = a.size/2 to 1; 这样的好处是对于大量的节点只需要比较1次或2次,那些需要比较lgn次的点非常少,这也就导致了build_heap可以在O(n)完成。
6,quick, 以上分析可知heap已经获得了很全面的性能,O(nlgn),且不需要merge的额外空间,但是他的常量系数不好(比如为了排序还得提前build_heap),这时候quick就出现了,当然还是采用divide and conquer思想,优势在于代码的实现非常紧凑,这就导致了常数因子较小,当然代价就是最坏情况下O(n^2)(no free lunch!),考虑到实际应用中的数据很少有极端(有强迫症的同学可以认为“小概率事件等于不可能发生事件”,所以quick得到了广泛的应用。再说点代码的小技巧(很多同学说算法的思路很清楚就是code不出来,其实就是一些小技巧你还不知道)
while(i<j)
{
while(i<j && a[j]>key)
j--;
a[i] = a[j];
while(i<j && a[i]<=key)
i++;
a[j] = a[i];
}
a,外层while已经有限制,而内层还需要添加此条件,与我们的习惯不是很相符,但注意到内层中的i and j 都是会变化的,所以必须添加此限制;b,要注意完备性,即分为两部分,<=key and > key .
7, 了解了这么多“比较排序”,自然就想知道他的下界。这里就有了“决策树”(不引入也可以,无非就是你需要通过“比较”这种操作去排序一组数,而“比较”操作就两种可能,你自己在比较的过程中自然就构造了一棵“决策树”),n个数据,有n!的排列,也就是叶子节点的数量,这棵树可以是相对平衡的,也可以是很畸形的,所谓的排序算法都可以对应一棵“决策树”,显然我们要追求的就是树高最小那棵树对应的算法,用到一点二叉树的东西,n! < 2^h, h下界为nlgn,换句话就是,最好的那棵树需要的比较操作为nlgn.
//sort alg summary
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
//O(n^2)
void bubble(vector<int>& a)
{
int n = a.size();
for (int i = n - 1; i >= 0; i--)
{
for (int j = 0; j < i; j++)
{
if (a[j] > a[j + 1])
swap(a[j], a[j + 1]);
}
}
}
//O(n^2)
void select(vector<int>& a)
{
int n = a.size();
for (int i = 0; i < n - 1; i++)
{
int min_index = i;
int min_val = a[i];
for (int j = i+1; j < n; j++)
{
if (a[j] < min_val)
{
min_val = a[j];
min_index = j;
}
}
swap(a[i], a[min_index]);
}
}
//O(n^2)
void insert(vector<int>& a)
{
int n = a.size();
for (int i = 0; i < n; i++)
{
int insert_val = a[i];
int j = i - 1;
while (j >= 0 && insert_val < a[j])
{
a[j+1] = a[j];
j--;
}
a[j + 1] = insert_val;
}
}
void merge(vector<int>& a, int start, int mid, int end)
{
vector<int> left(a.begin()+start,a.begin()+mid+1);
vector<int> right(a.begin()+mid+1,a.begin()+end+1);
//add sentinel
left.push_back(INT_MAX);
right.push_back(INT_MAX);
int idx_left = 0, idx_right = 0;
for (int i = start; i <= end; i++)
{
if (left[idx_left] < right[idx_right])
{
a[i] = left[idx_left];
idx_left++;
}
else
{
a[i] = right[idx_right];
idx_right++;
}
}
}
//O(nlgn)
void merge_sort(vector<int>& a, int start, int end)
{
if (start < end)
{
int mid = (start + end) / 2;
merge_sort(a, start, mid);
merge_sort(a, mid + 1, end);
merge(a, start, mid, end);
}
}
void max_heapify(vector<int>& a, int heap_size, int i)
{
int left = i * 2;
int right = left + 1;
int large = i;
if (left <= heap_size && a[left - 1] > a[large - 1])
large = left;
if (right <= heap_size && a[right-1] > a[large-1])
large = right;
if (large != i)
{
swap(a[i - 1], a[large - 1]);
max_heapify(a, heap_size, large);
}
}
void build_heap(vector<int>& a, int& heap_size)
{
heap_size = a.size();
for (int i = a.size() / 2; i > 0; i--)
max_heapify(a, heap_size, i);
}
//O(nlgn)
void heap_sort(vector<int>& a)
{
int heap_size;
build_heap(a,heap_size);
for (int i = a.size(); i > 0; i--)
{
swap(a[i - 1], a[1 - 1]);
heap_size--;
max_heapify(a, heap_size,1);
}
}
//O(nlgn)
int partion(vector<int>& a, int start, int end)
{
int key = a[start];
int i = start;
int j = end;
while (i<j)
{
while (i<j && a[j] > key)
j--;
a[i] = a[j];
while (i<j && a[i] <= key)
i++;
a[j] = a[i];
}
a[i] = key;
return i;
}
void quick(vector<int>& a, int start, int end)
{
if (start >= end)
return;
int p = partion(a, start, end);
quick(a, start, p - 1);
quick(a, p + 1, end);
}
int main(int argc, char** argv)
{
vector<int> array;
for (int i = 0; i < 10; i++)
array.push_back(rand() % 100);
cout << "old array" << endl;
for (auto val : array)
cout << val << " ";
cout << endl;
//bubble(array);
//select(array);
//insert(array);
//merge_sort(array,0, array.size()-1);
//heap_sort(array);
//quick(array, 0, array.size() - 1);
cout << "sorted array" << endl;
for (auto val : array)
cout << val << " ";
cout << endl;
return 0;
}