以下排序都从小到大排序,假设数列均为整数数列。
先从插入排序说起吧,插入排序是最基础的排序了,估计大学都讲烂了。
插入排序基本思想是将一个元素插入到已经排序的数列中,从而得到一个新的个数加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的值()。*/ } } }
然后再说说堆排序,堆排序是利用堆这种数据结构设计出来的一种算法。
堆排序使用完全二叉树作为基础模型,用数据将二叉树的数据存储。每个父节点都不大于其子节点的值,将二叉树的节点按照从上到下、从左到右的顺序依次放入数组中,根节点放在数组的开头即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); }
最后来看下快速排序,快速排序可以算是冒泡排序的一种改进。
快速排序的基本思想是在序列中选取一个元素作为参照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); }
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小的元素。