以下排序都从小到大排序,假设数列均为整数数列。
先从插入排序说起吧,插入排序是最基础的排序了,估计大学都讲烂了。
插入排序基本思想是将一个元素插入到已经排序的数列中,从而得到一个新的个数加1的序列。所以对一队无序序列进行插入排序,可以从第二个元素开始,依次插入前面的序列中,第一个元素可以看作是一个个数为1的有序序列。源码如下:
void insertionsort(int array[], int num)
{
int j, p;
int tmp;
for(p = 1; p < num; p++)
{
tmp = array[p];
for(j = p; j > 0 && array[j - 1] > tmp; j--)
{
array[j] = array[j - 1];
}
array[j] = tmp;
}
}
时间复杂度为O(N^2);
接下来再说说希尔排序,希尔排序算是插入排序的一个变种吧,他采用了一组增量因子,增量因子依次递减,直至为1。对于每个增量因子,都进行一系列排序,基本步骤是将相差增量因子N倍的元素列为1组,对该组进行插入排序,直到增量因子变为1,整个序列排序完成。源码如下:
/*希尔排序,增量因子取值为num/2*/
void shellsort(int array[], int num)
{
int i, j, tmp, increment, m;
increment = num / 2;
for(; increment > 0; increment /= 2)
{
for(i = increment; i < num; i++)
{
tmp = array[i];
for(j = i; j >= increment; j -= increment)
{
/*
每次只比较间隔为increment的两个元素。
如果不成立,就退出。如果成立,就继续比较
前边间隔为increment的元素(貌似没必要,因为
前面经过类似的步骤,前边的元素已经肯定是
排过序的了,肯定会经过else退出,所以j最多减
一次increment。)。
*/
if(tmp < array[j - increment])
array[j] = array[j - increment];
else
break;
}
array[j] = tmp; /*此时,j为减去increment的值()。*/
}
}
}
增量因子一般取值为数组个数的一般,还有一些其他的增量因子会使该算法的运行速度更快,但是也更为复杂,希尔排序的效率完全与增量因子的选取有关。由于希尔排序本质上是分组插入排序,保证了在相对较小的序列内进行插入排序,在相对较大的序列内对有序序列进行插入排序,所以他的效率比直接使用插入排序更优,在中等数据时会更明显。它的最坏时间复杂度为O(N^2),使用 Hibbard增量的希尔排序的时间复杂度为O(
然后再说说堆排序,堆排序是利用堆这种数据结构设计出来的一种算法。
堆排序使用完全二叉树作为基础模型,用数据将二叉树的数据存储。每个父节点都不大于其子节点的值,将二叉树的节点按照从上到下、从左到右的顺序依次放入数组中,根节点放在数组的开头即0的位置上,位置为i的节点其左子节点在数组中的位置为2*i+1,右子节点在数组中的位置为左子节点位置+1。
堆排序的基本思想是先建堆、然后通过不断地调整堆,完成对整个堆的排序。
建堆的过程通过不断地将父节点与两个子节点比较,将较大的值放入父节点,然后依次向下更新整个子树,从而完成整个树的更新。堆建立完成后,根节点应该是值最大的节点,其子树中也保持了父节点不小于两个子节点的特性。
对序列进行排序的过程就是在不断地调整堆。前面我们生成的堆中,对应到数组a里,a[0]存储的是根节点,即最大的值。我们将a[0]与数组最后一个元素互换,从而让最大值放置到数组最末尾,从二叉树来看,相当于将最底层的最右侧的节点放置到根节点上,根节点放置到最底层的最右侧的位置上,此时我们认为此节点已经被删除,后续二叉树调整中,不再处理此节点,所以二叉树的节点数减一。然后我们从根节点开始,再次更新二叉树,让较大值上移值根节点,从而再次建立一个满足原始特性的二叉树。然后继续将数组倒数第二个元素与a[0]互换,让第二大的元素,放置在数组倒数第二的位置上,重复以上的更新树活动。。不断地执行该过程,直到最后只剩根节点位置,此时,序列排序完成。源码如下:
/*
由于本次实例中堆对应的数组从0开始,所以
左子结点位置为2*i+1
*/
#define LEFTCHILD(i) (2 * (i) + 1)
/*堆排序*/
void percdown(int array[], int i, int num)
{
int child;
int tmp;
printf("i %d array: ", i);
/*每次循环都把更新child代表的位置,从而更新整个子树*/
for(tmp = array[i]; (child = LEFTCHILD(i)) < num; i = child)
{
/*查找最大子节点并且保证未越界*/
if(child != num - 1 && array[child + 1] > array[child])
{
child++; /*右子节点比较大*/
}
if(tmp < array[child]) /*当前节点小于子节点*/
{
array[i] = array[child];
}
else
break; /*子树满足要求,退出*/
}
array[i] = tmp;
#if 1
for(i = 0; i < 17; i++)
{
printf(" %d", array[i]);
}
printf("\n");
#endif
}
void heapsort(int array[], int num)
{
int i;
for(i = num / 2; i >= 0; i--)
percdown(array, i, num); /*创建堆*/
for(i = num - 1; i > 0; i--)
{
swap(&array[0], &array[i]); /*将首尾互换,保证最大值在队列最后*/
percdown(array, 0, i);
}
}
堆排序的平均时间复杂度为O(N*logN)。
接着,我们来看下归并排序。
归并排序的基本思想是将两个已经排好序的序列,合并排序到一个序列中。对于一个无序序列,最常见的是分治归并,通过采取二分法加递归来完成不同级别的子序列的排序及合并操作。
对于已经排序过的两个数组a、b,我们分别对a[i]和b[j]进行比较,若a[i]<=b[j],则将a[i]复制到数组c[k]中,然后将i加1,k加1,否则将b[j]复制到c[k]中,然后j、k分别加1。然后继续比较两者,如此循环,知道有一个列表元素复制完,则将另一个列表的剩余元素全部复制到c数组后面,从而两个序列合并排序到第三个数组序列中。源码如下:
void merge(int array[], int temp_array[],
int leftpos, int rightpos, int rightend)
{
int i, leftend, num, tmppos;
leftend = rightpos - 1;
tmppos = leftpos;
num = rightend - leftpos + 1;
while(leftpos <= leftend && rightpos <= rightend)
{
if(array[leftpos] <= array[rightpos])
temp_array[tmppos++] = array[leftpos++];
else
temp_array[tmppos++] = array[rightpos++];
}
while(leftpos <= leftend) /*左侧剩余*/
temp_array[tmppos++] = array[leftpos++];
while(rightpos <= rightend)
temp_array[tmppos++] = array[rightpos++];
/* Copy TmpArray back */
for( i = 0; i < num; i++, rightend-- )
array[ rightend ] = temp_array[ rightend ];
}
void msort(int array[], int tmparray[], int left, int right)
{
int center;
if(left < right)
{
center = (right + left) / 2;
msort(array, tmparray, left, center);
msort(array, tmparray, center + 1, right);
merge(array, tmparray, left, center + 1, right);
}
}
void mergesort(int array[], int num)
{
int *tmp = NULL;
tmp = (int *)malloc(sizeof(int) * num);
if(tmp == NULL)
{
printf("malloc failed\n");
}
msort(array, tmp, 0, num - 1);
free(tmp);
}
由于涉及到递归调用,我们在合并之前,动态申请了一块内存,从而避免在每次递归调用的时候都从栈中开辟临时存储空间,提升效率。
时间复杂度为O(N*logN)。
最后来看下快速排序,快速排序可以算是冒泡排序的一种改进。
快速排序的基本思想是在序列中选取一个元素作为参照KEY值,然后分别从序列的两头向该元素方向检索。在左侧遇到大于KEY值的元素,停止左侧检索,在右侧遇到小于KEY值的元素,停止右侧检索。当两侧的检索都停止时,如果两个检索指针还未到达KEY值处,则互换当前两个检索指针指向的两个元素,如果已经到达或是已经越过,则该轮快速排序完成。此时KEY左侧的元素均小于KEY值,右侧的元素均大于KEY值。
通过递归调用二分法,将整个序列分割成最小的序列,然后使用插入排序或是其他方法进行排序,保证最小序列有序后,不断地与上级回调合并,从而达到有整个序列排序的效果。源码如下:
#define CUTOFF 3
void swap(int *a, int *b)
{
*a ^= *b;
*b ^= *a;
*a ^= *b;
}
int mid3(int array[], int left, int right)
{
int center = (left + right) / 2;
if(array[left] > array[center])
swap(&array[left], &array[center]);
if(array[left] > array[right])
swap(&array[left], &array[right]);
if(array[center] > array[right])
swap(&array[center], &array[right]);
swap(&array[center], &array[right - 1]);
return array[right - 1];
}
void Q_sort(int array[], int left, int right)
{
int i, j, pivot;
if(left + CUTOFF <= right)
{
pivot = mid3(array, left, right);
i = left;
j = right - 1; /*取完中值后,right处是肯定大于KEY的值,所以可以从right-1处开始检索*/
for( ; ; )
{
while(array[++i] < pivot) {}
while(array[--j] > pivot) {}
if(i < j)
swap(&array[i], &array[j]);
else
break;
}
if(i != right -1) /*因为我们通过异或来交互,所以要确保两者不是同一个数*/
swap(&array[i], &array[right - 1]);
Q_sort(array, left, i - 1);
Q_sort(array, i + 1, right);
}
else
insertionsort(array + left, right - left + 1);
}
void quicksort(int array[], int num)
{
Q_sort(array, 0, num - 1);
}
对于快速排序,合理的选择KEY值是个关键。一般来说有以下三种方法:
1、选取第一个元素。该方法对于随机序列来说是没有问题的,当时如果是对于一个反序的序列,会产生非常劣质的分割,从而导致在所有的递归调用中,均使用了最坏的时间量。该做法不应该随意使用。
2、随机选取元素。一般来说该种策略是安全的,但是对于随机数的产生也是非常昂贵的,在递归调用中,每次都必须先产生随机数也会是个不小的开销。
3、中值分割法。一般来说,我们选取第N/2大的元素作为KEY值,但是该值是比较难算出的。通常,我们选取队列的三个元素,使用这三个元素的中值来作为KEY值。我们可以随机选取三个元素,但是因为使用到随机,所以该方法也是不会有多大帮助。所以我们选取序列的开头、结尾和中间这三个元素进行比较。将三者中最小值放置到序列开头,次小值放置到中间,最大值放置到结尾。
快速排序的平均运行时间为O(N*logN),最坏为O(N^2)。
可以看出在快速排序法中,每完成一次快速排序,key值所正在位置i,正好该序列中第i大的元素。因为每完成一次,在i左侧的都比i小或等于,在i右侧的都比i大或等于。
由此,我们可以想出一个快速选择序列第i小元素的方法。源码如下:
void quickselect(int array[], int k, int left, int right)
{
int i, j, pivot;
if(left + CUTOFF <= right)
{
pivot = mid3(array, left, right);
i = left;
j = right - 2;
for( ; ; )
{
while(array[++i] < pivot) {}
while(array[--j] > pivot) {}
if(i < j)
swap(&array[i], &array[j]);
else
break;
}
if(i != right -1)
swap(&array[i], &array[right - 1]);
if(k < i + 1) /*比当前i位置小,还需对左侧进行排序*/
quickselect(array, k, left, i - 1);
else if(k > i + 1) /*比当前i位置大,还需对右侧进行排序*/
quickselect(array, k, i + 1, right);
}
else
insertionsort(array + left, right - left + 1);
}
运行停止后,第K位置上就是该序列第K小的元素。