排序是计算机内经常进行的一种操作,其目的是将一组“无序”的记录序列调整为“有序”的记录序列。
排序分为内部排序和外部排序。
若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序。
反之,若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序。
/*
* 冒泡排序
* 相邻元素比较,大的元素往后调
*/
public static void bubbleSort(int array[]){
for(int i = array.length - 1 ; i >= 0 ; i--){
boolean flag = false; //设置一趟排序是否有交换的标识
for(int j = 0 ; j < i ; j++){ //一趟冒泡排序
if(array[j] > array[j+1]){
swap(array, j, j+1);
flag = true; //标识发生了交换
}
}
if(!flag)
break;
}
}
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对 n个元素的表进行排序总共进行至多 n-1 次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。
/*
* 选择排序
* 每个位置选择当前元素最小的
*/
public static void selectSort(int array[]){
for(int i = 0 ; i < array.length-1 ; i++){
int minPosition = i;
int min = array[i];
for(int j = i+1 ; j <array.length ; j++){
if(array[j] < min){
min = array[j];
minPosition = j;
}
}
//若i不是当前元素最小的,则和找到的那个元素交换
if(i != minPosition){
array[minPosition] = array[i];
array[i] = min;
}
}
}
/*
* 插入排序
* 已经有序的小序列的基础上,一次插入一个元素
*/
public static void insertSort(int array[]){
for(int i = 1 ; i < array.length ; i++){
int current = array[i]; //待排元素
int j = i;
for(; j > 0 && array[j - 1] > current ; j--){
//向前扫描,只要发现待排元素比较小,就插入
array[j] = array[j - 1]; //移出空位
}
array[j] = current; //元素插入
}
}
快速排序的基本思想:挖坑填数+分治法。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。虽然 Worst Case 的时间复杂度达到了 O(n²),但是人家就是优秀,在大多数情况下都比平均时间复杂度为 O(n logn) 的排序算法表现要更好。
快速排序使用分治策略来把一个序列(list)分为两个子序列(sub-lists)。步骤为:
递归到最底部时,数列的大小是零或一,也就是已经排序好了。这个算法一定会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
/*
* 快速排序
* 两个方向,左边的i下标一直往右走,当a[i] <= a[center_index],
* 其中center_index是中枢元素的数组下标,一般取为数组第0个元素。而右边的j下标一直往左走,当a[j] > a[center_index]
* 如果i和j都走不动了,i <= j, 交换a[i]和a[j],重复上面的过程,直到i>j
* 交换a[j]和a[center_index],完成一趟快速排序
* 枢轴采用三数取中法可以优化
*/
//递归快速排序
public static void quickSort(int a[]){
qSort(a, 0, a.length - 1);
}
//非递归快速排序,手动利用栈来存储每次分块快排的起始点,栈非空时循环获取中轴入栈
public static void quickSortNonRecursion(int array[]){
if (array == null || array.length == 1) return;
//存放开始与结束索引
Stack<Integer> s = new Stack<Integer>();
//压栈
s.push(0);
s.push(array.length - 1);
//利用循环里实现
while (!s.empty()) {
int right = s.pop();
int left = s.pop();
//如果最大索引小于等于左边索引,说明结束了
if (right <= left) continue;
int i = partition(array, left, right);
if (left < i - 1) {
s.push(left);
s.push(i - 1);
}
if (i + 1 < right) {
s.push(i+1);
s.push(right);
}
}
}
//递归排序,利用两路划分
public static void qSort(int a[],int low,int high){
int pivot = 0;
if(low < high){
//将数组一分为二
pivot = partition(a,low,high);
//对第一部分进行递归排序
qSort(a,low,pivot);
//对第二部分进行递归排序
qSort(a,pivot + 1,high);
}
}
//partition函数
public static int partition(int a[],int low,int high){
int pivotkey = a[low]; //选取第一个元素为枢轴记录
while(low < high){
//将比枢轴记录小的交换到低端
while(low < high && a[high] >= pivotkey){
high--;
}
//采用替换而不是交换的方式操作
a[low] = a[high];
//将比枢轴记录大的交换到高端
while(low < high && a[low] <= pivotkey){
low++;
}
a[high] = a[low];
}
//枢纽所在位置赋值
a[low] = pivotkey;
//返回枢纽所在的位置
return low;
}
归并排序可通过两种方式实现:
递归法(假设序列共有n个元素):
迭代法
/*
* 归并排序
* 把序列递归地分成短序列
* 递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),
* 然后把各个有序的短序列合并成一个有序的长序列,不断合并直到原序列全部排好序
*/
//将有二个有序数列a[first...mid]和a[mid+1...last]合并。
public static void merge(int a[], int first, int mid, int last, int temp[]){
int i = first,j = mid+1;
int k = 0;
while(i <= mid && j<= last){
if(a[i]<a[j])
temp[k++] = a[i++];
else
temp[k++] = a[j++];
}
while(i <= mid)
temp[k++] = a[i++];
while(j <= last)
temp[k++] = a[j++];
for(i = 0 ; i < k ; i++)
a[first+i] = temp[i];
}
//递归合并排序
public static void mSort(int a[], int first,int last, int temp[]){
if(first < last){
int mid = (first + last) / 2;
mSort(a, first, mid, temp);
mSort(a, mid+1, last, temp);
merge(a, first, mid, last, temp);
}
}
//提供通用归并排序接口
public static void mergeSort(int a[]){
int[] temp = new int[a.length];
mSort(a, 0, a.length-1, temp);
}
将待排序数组按照步长gap进行分组,然后将每组的元素利用直接插入排序的方法进行排序;每次再将gap折半减小,循环上述操作;当gap=1时,利用直接插入,完成排序。
可以看到步长的选择是希尔排序的重要部分。只要最终步长为1任何步长序列都可以工作。一般来说最简单的步长取值是初次取数组长度的一半为增量,之后每次再减半,直到增量为1。
/*
* 希尔排序
* 按照不同步长对元素进行插入排序
* 插入排序的一种
*/
public static void shellSort(int a[]){
if(a == null || a.length == 0){
return;
}
int len = a.length;
//初始化增量
int inc = len;
do{
//增量变化规则
inc = inc / 3 + 1;
for(int i = inc; i < len; i++){
//待排元素
int cur = a[i];
int j = i;
//向前扫描,只要发现待排元素比较小,就插入
for(; j >= inc && a[j - inc] > cur; j -= inc){
//移除空位
a[j] = a[j - inc];
}
//元素插入
a[j] = cur;
}
}while(inc > 1);
}
1991年的计算机先驱奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德(Robert W.Floyd) 和威廉姆斯(J.Williams) 在1964年共同发明了著名的堆排序算法(Heap Sort)
/*
* 堆排序
* 调整最大堆,交换根元素和最后一个元素。
* 参数说明:
* a -- 待排序的数组
*/
public static void heapSort(int[] a) {
if(a == null || a.length == 0){
return;
}
int len = a.length;
//从尾部开始,调整成最大堆
for(int i = len / 2 - 1; i >= 0; i--){
maxHeapDown(a, i, len - 1);
}
//从最后一个元素开始对序列进行调整,不断缩小调整的范围直到第一个元素
for(int i = len - 1; i >= 0; i--){
//交换a[0]和a[i]。交换后,a[i]是a[0..i]中最大
int tmp = a[0];
a[0] = a[i];
a[i] = tmp;
//调整a[0..i - 1],使得a[0..i - 1]仍然是一个最大堆
maxHeapDown(a, 0, i - 1);
}
}
/*
* 注:数组实现的堆中,第N个节点的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。
* 其中,N为数组下标索引值,如数组中第1个数对应的N为0。
*
* 参数说明:
* a -- 待排序的数组
* lo -- 被下调节点的起始位置(一般为0,表示从第1个开始)
* hi -- 截至范围(一般为数组中最后一个元素的索引)
*/
private static void maxHeapDown(int[] a, int lo, int hi){
//记录当前结点位置
int curIndex = lo;
//记录左孩子结点
int left = 2 * curIndex + 1;
//记录当前结点的值
int curVal = a[curIndex];
//保证curIndex,leftIndex,rightIndex中,curIndex对应的值最大
for(; left <= hi; curIndex = left, left = 2 * left + 1){
//左右孩子中选择较大者
if(left < hi && a[left] < a[left + 1]){
left++;
}
if(curVal >= a[left]){
break;
}else{
a[curIndex] = a[left];
a[left] = curVal;
}
}
}
基数排序按照优先从高位或低位来排序有两种实现方案:
我们以LSD为例,从最低位开始,具体算法描述如下:
基数排序:通过序列中各个元素的值,对排序的N个元素进行若干趟的“分配”与“收集”来实现排序。
/*
* 基数排序
* 按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位
*/
public static void radixSort(int[] array,int d)
{
int n=1; //代表位数对应的数:1,10,100...
int k=0; //保存每一位排序后的结果用于下一位的排序输入
int length=array.length;
int[][] bucket=new int[10][length]; //排序桶用于保存每次排序后的结果,这一位上排序结果相同的数字放在同一个桶里
int[] order=new int[length]; //用于保存每个桶里有多少个数字
while(n<d)
{
for(int num:array) //将数组array里的每个数字放在相应的桶里
{
int digit=(num/n)%10;
bucket[digit][order[digit]]=num;
order[digit]++;
}
for(int i=0;i<length;i++) //将前一个循环生成的桶里的数据覆盖到原数组中用于保存这一位的排序结果
{
if(order[i]!=0) //这个桶里有数据,从上到下遍历这个桶并将数据保存到原数组中
{
for(int j=0;j<order[i];j++)
{
array[k]=bucket[i][j];
k++;
}
}
order[i]=0; //将桶里计数器置0,用于下一次位排序
}
n*=10;
k=0; //将k置0,用于下一轮保存位排序结果
}
}
基数排序更适合用于对时间, 字符串等这些 整体权值未知的数据 进行排序。
基数排序不改变相同元素之间的相对顺序,因此它是稳定的排序算法。
基数排序 vs 计数排序 vs 桶排序
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
基数排序:根据键值的每位数字来分配桶
计数排序:每个桶只存储单一键值
桶排序:每个桶存储一定范围的数值
各种排序性能对比如下:
排序类型 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 |
选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 |
直接插入排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 |
折半插入排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 |
希尔排序 | O(n^1.3) | O(nlogn) | O(n²) | O(1) | 不稳定 |
归并排序 | O(nlog₂n) | O(nlog₂n) | O(nlog₂n) | O(n) | 稳定 |
快速排序 | O(nlog₂n) | O(nlog₂n) | O(n²) | O(nlog₂n) | 不稳定 |
堆排序 | O(nlog₂n) | O(nlog₂n) | O(nlog₂n) | O(1) | 不稳定 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(k) | 稳定 |
桶排序 | O(n+k) | O(n+k) | O(n²) | O(n+k) | (不)稳定 |
基数排序 | O(d(n+k)) | O(d(n+k)) | O(d(n+kd)) | O(n+kd) | 稳定 |
从时间复杂度来说:
论是否有序的影响: