——位运算应用篇(3)
摘要
本篇仍然关注位操作的应用,通过前面的两篇文章(《非常给力:位运算求组合》,《简单、易懂:位运算之求集合的所有子集》),我们已经略见了位操作之强大威力。如果说那两篇文章中讲解的应用比较偏僻,那么本篇介绍的是位操作在广为人知的排序算法中的应用——位排序。本篇讲述了位排序的两种实现方法,在描述上尽量做到简单、通俗、易懂。
本篇内容并不新颖,如果您关注过大量数据处理或者看过各大牛X公司的面试系列或者读过《编程珠玑》,那么应该对位排序非常熟悉,即便是这样,本篇作者仍然希望你挤出一些你的宝贵时间来看完后面的内容,因为作者非常希望您能提一些意见,或者更正一些错误。
引例
在切入正题之前,我们来看一个引例:有一个数组,乱序存放了某个班级的所有学生的学号,已知该班级的学生数目不超过50人,并且学号范围是0到49(不一定连续,因为有些同学可能中途被开除了,作者本人就是导致当年那个大学班级的学号不连续的第一人,详情见《有感于“要像狗一样活下去”》),现在请你对这个数组排序,要求时间复杂度必须小于O(nlog2(n))。
上面的引例并不属于大量数据处理问题,不是用来做位排序的好例子,但是它却是个引子,因为其时间复杂度的特殊要求。后面我们将看到位排序实际上适合处理大量数据排序问题。
最初的尝试
一看到这样的题目,我们的大方向是明确的,那就是排序呗,于是我们开始尝试各种主流的排序算法,看看能否满足要求。最简单、最基础、也是最容易实现的冒泡排序、插入排序、选择排序时间复杂度为均O(n2),不满足要求。高级的排序算法如快速排序、堆排序、归并排序、希尔排序等时间复杂度为O(nlog2(n)),仍然达不到时间复杂度小于O(nlog2(n))的要求。事实上绝大多数数据结构教程上都说了最好的排序算法时间复杂度是O(nlog2(n))。没错,确实如此,因为它们讲解的是通用的排序算法,而这里的引例有几个特殊条件可供我们使用:
1、 待排序的元素的最大范围已知(从0到49)
2、 待排序的元素不重复,因为不存在两个学号相等的学生(非常隐蔽的、但是非常重要的条件)
3、 待排序数据基本充满整个数据范围(只有少数人会被开除)(这一点也是非常重要的,后面我会解释)
4、 小于O(nlog2(n))的时间复杂度(与其说这是可以利用的条件,还不如说这是孙悟空头上的紧箍咒)
既然常用的排序算法不能满足要求,那么我们就得想其他方法了。我们都知道算法的时间复杂度和空间复杂度是一对矛盾体,通常可以采取以空间换时间的方法来降低算法的时间复杂度。后面讲的函数(sort_1)是我大一时想出来的,当时是为了解决数据结构上的课后习题,还记得当初为自己的这个实现激动了许久。在给出程序之前,先说一下我当时的思路:1、用一个标记数组记录每一个数是否存在于待排序数组中,如果存在而标记1,否则标记0;2、扫描标记数组,并根据它重新填充原来的待排序数组。我的程序看起来是这样的
using namespace std;
#define NUMBER_RANGE 50
#define ARRAY_SIZE 10
int a[ARRAY_SIZE] ={1,13,3,42,7,23,5,37,26,10};
void sort_1(int a[],int N) //N是a中待排序元素个数
{
int i,j;
int* p = new int[NUMBER_RANGE]; //这里浪费了40*4=160byte
memset(p,0,NUMBER_RANGE