本文记录了我在面试过程中感觉有用的问题,方便日后参考。
前言
一、排序的目的
为了查找方便,通常希望计算机中的查找表是按关键字有序的,因为此时可以使用查找效率较高的折半查找。
二、排序的概念
将一个数据元素的任意序列,重新排列成一个按关键字有序的序列。
三、内部排序和外部排序
根据排序时待排序的数据元素数量的不同,使得排序过程中涉及的存储器不同,可以将排序方法分为两类。
一类是整个排序过程在内存储器中进行,称为内部排序;另一类是由于待排序元素数量太大,
以至于内存储器无法容纳全部数据,排序需要借助外部存储设备才能完成,这类排序称为外部排序。
本文涉及的排序均为内部排序。
四、排序结果是否唯一
如果是按照主关键字来排序,则得到的排序结果是唯一的。否则,如果按照次关键字,则得到的结果是不唯一的。
五、排序是否稳定
如果在待排序的序列中存在多个具有相同关键字的元素。假设元素 Ri 在 Rj 之前,两者关键字
相同,如果排序过后 Ri 仍在 Rj 前面,则称排序方法是稳定的,否则,当相同关键字元素的
前后关系在排序中发生变化,则称排序方法是不稳定的。无论是稳定的还是不稳定的排序方法,
均能完成排序的功能。在某些场合可能对排序有稳定性的要求,此时就应当选择稳定的排序方法。
六、排序的分类
按照排序过程中依据的原则对内部排序进行分类,大致可以分为插入排序、交换排序、选择排序、
归并排序等。
插入类排序:
基本思想:逐个考察每个待排序元素,将每一个新元素插入到前面已经排好序的序列中适当的位置上,
使得新序列仍然是一个有序序列。类别:直接插入排序、折半插入排序、希尔排序。
交换类排序主要是通过两两比较待排元素的关键字,若发现与排序要求相逆,则“交换”
之。在这类排序方法中最常见的是起泡排序和快速排序,其中快速排序是一种在实际应用中
具有很好表现的算法。
选择排序的基本思想是:每一趟从n-i+1 (i=1,2,…,n)个元素中选取一个关键字最小的元
素作为有序序列中第i 个元素。本节在介绍简单选择排序的基础上,给出了对其进行改进的
算法——树型选择排序和堆排序。
七、基于比较的排序的对比
插入排序、交换排序、选择排序、归并排序等排序方法,都有一个共同的特点,那就是它们都是通过
比较元素的大小来确定元素之间的相对位置的,都是基于比较的排序方法。
从算法的平均时间复杂度、最坏时间复杂度、空间复杂度以及排序的稳定性等方面,对各种排序方法加以比较。
排序方法 | 平均时间复杂度 | 最坏时间复杂度 | 最好时间复杂度 | 空间复杂度 | 稳定性 | 复杂性 |
---|---|---|---|---|---|---|
直接插入排序 | O(\(n^2\)) | O(\(n^2\)) | O(n) | O(1) | 稳定 | 简单 |
希尔排序 | O(\(nlog_2n\)) | O(\(nlog_2n\)) | O(1) | 不稳定 | 较复杂 | |
冒泡排序 | O(\(n^2\)) | O(\(n^2\)) | O(n) | O(1) | 稳定 | 简单 |
快速排序 | O(\(nlog_2n\)) | O(\(n^2\)) | O(\(nlog_2n\)) | O(\(nlog_2n\)) | 不稳定 | 较复杂 |
直接选择排序 | O(\(n^2\)) | O(\(n^2\)) | O(\(n^2\)) | O(1) | 不稳定 | 简单 |
堆排序 | O(\(nlog_2n\)) | O(\(nlog_2n\)) | O(\(nlog_2n\)) | O(1) | 不稳定 | 较复杂 |
归并排序 | O(\(nlog_2n\)) | O(\(nlog_2n\)) | O(\(nlog_2n\)) | O(n) | 稳定 | 较复杂 |
基数排序 | O(d(n+r)) | O(d(n+r)) | O(d(n+r)) | O(n+r) | 稳定 | 较复杂 |
通过对排序可能出现的结果个数的研究,可以得出如下结论:
任何一个基于比较操作的排序方法,在最坏情况下所需要进行的比较次数至少为 nlogn 次,
即算法的时间复杂度下界为 Ω(nlogn)。
详细介绍
直接插入排序(简单插入排序)代码
基本思想:在要排序的一组数中,假设前面(n-1) [n>=2] 个数已经是排好顺序的,
现在要把第n个数插到前面的有序数中,使得这n个数也是排好顺序的。
如此反复循环,直到全部排好顺序。
public static void insertSort(int[] r, int low, int high){
for(int tmp, j, i = low + 1; i <= high; i++){
if(r[i] < r[i-1]){
tmp = r[i];
j = i - 1;
for(; j >= low && tmp < r[j]; j--)
r[j+1] = r[j];
r[j+1] = tmp;
}
}
}
希尔排序(最小增量排序)
基本思想:算法先将要排序的一组数按某个增量d(n/2,n为要排序数的个数)分成若干组,
每组中记录的下标相差d.对每组中全部元素进行直接插入排序,然后再用一个较小的
增量(d/2)对它进行分组,在每组中再进行直接插入排序。当增量减到1时,
进行直接插入排序后,排序完成。
//method one
public static void shellSort(int[] r, int low, int high){
for(int gap = r.length/2; gap > 0; gap /= 2){
for(int m = 0; m < gap; m++){
for(int tmp, j, i = low + gap; i <= high; i += gap){
if(r[i] < r[i-gap]){
tmp = r[i];
j = i - gap;
for(; j >= low && tmp < r[j]; j -= gap)
r[j+gap] = r[j];
r[j+gap] = tmp;
}
}
}
}
}
//method two
public static void shellSortII(int[] r, int low, int high){
for(int gap = r.length/2; gap > 0; gap /= 2){
for(int tmp, j, i = gap; i <= high; i++){
if(r[i] < r[i-gap]){
tmp = r[i];
j = i - gap;
for(; j >= low && tmp < r[j]; j -= gap)
r[j+gap] = r[j];
r[j+gap] = tmp;
}
}
}
}
//method three
public static void shellSortIII(int[] r, int low, int high){
for(int gap = r.length/2; gap > 0; gap /= 2){
for(int i = gap; i <= high; i++){
for(int j = i-gap; j >= 0 && r[j] > r[j+gap]; j -= gap)
swap(r, j, j+gap);
}
}
}
private static void swap(int[] r, int i, int j){
int tmp = r[i];
r[i] = r[j];
r[j] = tmp;
}
冒泡排序代码
基本思想:在要排序的一组数中,对当前还未排好序的范围内的全部数,
自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,
较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。
public static void bubbleSort(int[] r, int low, int high){
for(int i = low; i <= high; i++){
for(int j = i+1; j <= high; j++){
if(r[i] > r[j]){
swap(r, i, j);
}
}
}
}
private static void swap(int[] r, int i, int j){
int tmp = r[i];
r[i] = r[j];
r[j] = tmp;
}
快速排序代码
基本思想:选择一个基准元素,通常选择第一个元素或者最后一个元素,通过一趟扫描,
将待排序列分成两部分,一部分比基准元素小,一部分大于等于基准元素,
此时基准元素在其排好序后的正确位置,然后再用同样的方法递归地排序划分的两部分。
public static void quickSort(int[] r, int low, int high) {
if (low < high) {
int pa = partition(r, low, high);
quickSort(r, low, pa - 1);
quickSort(r, pa + 1, high);
}
}
private static int partition(int[] r, int low, int high) {
int pivot = r[low]; // 使用r[low]作为枢轴元素
while (low < high) { // 从两端交替向内扫描
while (low < high && r[high] > pivot)
high--;
r[low] = r[high]; // 将比pivot 小的元素移向低端
while (low < high && r[low] < pivot)
low++;
r[high] = r[low]; // 将比pivot 大的元素移向高端
}
r[low] = pivot; // 设置枢轴
return low; // 返回枢轴元素位置
}
直接选择排序(简单选择排序)
基本思想:在要排序的一组数中,选出最小的一个数与第一个位置的数交换;
然后在剩下的数当中再找最小的与第二个位置的数交换,如此循环到倒数第二个数和最后一个数比较为止。
public static void selectSort(int r[], int low, int high){
for(int i = low; i <= high; i++){
int min = i;
for(int j = i+1; j <= high; j++){
if(r[j] < r[min]){
min = j;
}
}
swap(r, i, min);
}
}
private static void swap(int[] r, int i, int j){
int tmp = r[i];
r[i] = r[j];
r[j] = tmp;
}
树形选择排序(锦标赛排序)
基本思想: 先把待排序的 n 个元素两两进行比较,取出较小者,若轮空则直接进入下一轮比较;
然后在[n/2]个较小者中,采用同样的方法进行比较,再选出较小者;如此反复,直到选出关键字最小的元素为止。
这个过程可以使用一颗具有 n 个结点的完全二叉树来表示,最终选出的关键字最小的元素就是这棵二叉树的根结点。
在输出关键字最小的元素后,为选出次小关键字,可以将最小关键字元素所对应的叶子结点的关键字设置为∞,
然后从该叶子结点起逆行向上,将所经过的结点与其兄弟进行比较,修改从该叶子结点到根结点上各结点的值,则根结点的值即为次小关键字。
public static void treeSelectSort(int[] r, int low, int high){
int len = high - low + 1;
int treeSize = 2 * len - 1;
int[] tree = new int[treeSize];
//fill leaf nodes
for(int i=len-1, j=0; i >= 0; i--, j++){
tree[treeSize-1-j] = r[i];
}
//fill non-leaf nodes
for(int i=treeSize-1; i > 0; i -= 2){
tree[(i-1)/2] = tree[i-1] < tree[i] ? tree[i-1] : tree[i];
}
int minIndex;
while(low <= high){
int min = tree[0];
r[low++] = min;
minIndex = treeSize - 1;
while(tree[minIndex] != min)
minIndex--;
tree[minIndex] = Integer.MAX_VALUE;
while(minIndex > 0){ //if it has parent node
if(minIndex % 2 == 0){ //if it's the right node
tree[(minIndex-1)/2] = tree[minIndex-1] < tree[minIndex] ? tree[minIndex-1] : tree[minIndex];
minIndex = (minIndex - 1)/2;
}
else { //if it's the left node
tree[minIndex/2] = tree[minIndex] < tree[minIndex+1] ? tree[minIndex] : tree[minIndex+1];
minIndex = minIndex/2;
}
}
}
}
堆排序
基本思想: 设有n 个元素,欲将其按关键字排序。可以首先将这n 个元素按关键字建成堆,将堆顶
元素输出,得到n 个元素中关键字最大(或最小)的元素。然后,再将剩下的n-1 个元素重
新建成堆,再输出堆顶元素,得到n 个元素中关键字次大(或次小)的元素。如此反复执行,
直到最后只剩一个元素,则可以得到一个有序序列,这个排序过程称之为堆排序。
public static void heapSort(int[] r, int low, int high){
for(int i=high/2; i >= 0; i--)
heapAdjust(r, i, high);
int tmp;
for(int i=high; i >= 0; i--){
tmp = r[0];
r[0] = r[i];
r[i] = tmp;
heapAdjust(r, 0, i-1);
}
}
private static void heapAdjust(int[] r, int low, int high){
int tmp = r[low];
for(int j=2*low+1; j <= high; j *= 2){
if(j < high && r[j] < r[j+1])
j++;
if(tmp >= r[j])
break;
r[low] = r[j];
low = j;
}
r[low] = tmp;
}
归并排序
基本思想: 归并排序是建立在归并操作上的一种有效的排序算法,是采用分治法(divide and conquer)的一个
典型应用。先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
public static void mergeSort(int[] r, int low, int high){
if(low < high){
int center = (low+high)/2;
mergeSort(r, low, center);
mergeSort(r, center+1, high);
merge(r, low, center, high);
}
}
private static void merge(int[] r, int left, int center, int right){
int[] tmpArr = new int[right-left+1];
int middle = center + 1;
int start = 0;
int index = left;
while(left <= center && middle <= right){
if(r[left] <= r[middle]){
tmpArr[start++] = r[left++];
}else {
tmpArr[start++] = r[middle++];
}
}
while(left <= center)
tmpArr[start++] = r[left++];
while(middle <= right)
tmpArr[start++] = r[middle++];
for(int i=0; i < tmpArr.length; i++)
r[index+i] = tmpArr[i];
}
//归并排序自底向上的方式
/*
1 xx|xx|xx|xx|xx|xx|xx|xx|
2 xxxx|xxxx|xxxx|xxxx|
3 xxxxxxxx|xxxxxxxx
*/
public static void bottomUpMergeSort(int[] r, int low, int high){
int size = high - low + 1;
//外循环为每次归并排序,每组数据的宽度,每组数据的宽度之后进行2倍递增
for(int width=1; width < size; width *= 2){
//内循环为基于每组数据的宽度,进行多组数据的归并排序
//index += 2 * width 因为一次归并排序都是使用 2 组数据进行排序,
//所以每次递增两组数据的偏移量
//index < (size - width) 表示排序至少需要一组多的数据
for(int index=0; index < (size-width); index += 2*width){
int lo = index;
int hi = index + (2 * width - 1);
int mid = index + (hi - lo)/2;
merge(r, lo, mid, hi);
}
}
}
计数排序
基本思想: 计数排序是一种类似桶排序的算法,时间复杂度 O(n),其优势是对已知数量范围
的数组进行排序。它创建一个长度为这个数据范围的数组,这个数组的每个元素记录排序数组
中对应记录出现的个数。
//方案一,来自麻省的教材
public static void countingSort(int[] in, int[] out, int k){
int[] temp = new int[k+1];
for(int i=0; i < in.length; i++)
temp[in[i]]++;
for(int i=1; i <= k; i++)
temp[i] += temp[i-1];
for(int i=in.length-1; i >= 0; i--){
out[temp[in[i]]-1] = in[i];
temp[in[i]]--;
}
}
//方案二
public static void countingSortII(int[] r, int k){
int[] temp = new int[k+1];
for(int i=0; i < r.length; i++)
temp[r[i]]++;
for(int z=0, i=0; i <= k; i++){
while(temp[i]-- > 0)
r[z++] = i;
}
}
桶排序(箱排序)
基本思想: 设待排序序列的元素取值范围为 0 到 m ,则我们新建一个大小为 m+1 的临时数组并把初始值都设为0,
遍历待排序序列,把待排序序列中元素的值作为临时数组的下标,找出临时数组中对应该下标的元素使之加1;
然后遍历临时数组,把临时数组中元素大于 0 的下标作为值按次序依次填入待排序数组,元素的值作为重复填入该下标的次数,
遍历完成则排序结束序列有序。
public static void bucketSort(int[] r, int max){
int[] tmp = new int[max+1];
for(int i=0; i < r.length; i++)
tmp[r[i]]++;
for(int i=0, j=0; i <= max; i++)
for(int k=0; k < tmp[i]; k++)
r[j++] = i;
}
基数排序
基本思想: 基数排序不需要比较关键字的大小,它是根据关键字中各位的值,通过对排序的 n 个元素进行
若干趟“分配”与“收集”来实现排序的。
//获取指定位上的数字
private static int getDigit(int x, int d){
int value = 1;
for(int i=1; i < d; i++)
value *= 10;
return (x/value) % 10;
}
public static void radixSort(int[] r, int low, int high, int digit){
final int radix = 10; //基数
int[] count = new int[radix]; //存放各个桶数据的统计个数
int[] bucket = new int[high-low+1]; //桶
//从低位到高位排序
for(int i, j, d=1; d <= digit; d++){
//置空各个桶的数据统计
for(i=0; i < radix; i++)
count[i] = 0;
//统计各个桶的数据个数
for(i=low; i <= high; i++){
j = getDigit(r[i], d);
count[j]++;
}
//累计小于桶数据 i 的数据个数
for(i=1; i < radix; i++)
count[i] = count[i] + count[i-1];
//从右向左装桶,保证数据稳定性
for(i=high; i >= low; i--){
j = getDigit(r[i], d);
bucket[count[j]-1] = r[i];
count[j]--;
}
//倒出桶中的数据
for(i=low, j=0; i <= high; i++, j++)
r[i] = bucket[j];
}
}