常见的排序算法主要有:冒泡排序、插入排序、选择排序、快速排序、归并排序、堆排序、基数排序。
各种排序算法的时间空间复杂度、稳定性见下图
类别 | 排序方法 | 时间复杂度 | 空间复杂度 | 稳定性 | ||
最好 | 最坏 | 平均 | ||||
交换类排序 | 冒泡排序 | 稳定 | ||||
快速排序 | | 不稳定 | ||||
选择类排序 | 选择排序 | 不稳定 | ||||
堆排序排序 | | 不稳定 | ||||
插入类类排序 | 插入排序 | 稳定 | ||||
希尔排序 | 不稳定 | |||||
基数排序 | 稳定 | |||||
归并排序 | 稳定 | |||||
n:代表关键字的个数,r:代表关键字的基数,d代表关键字的长度 |
冒泡排序
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
思路:
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最
- 大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;
- 重复步骤1~3,直到排序完成。
代码实现
public void bubbleSort(int[] arr) {
if (arr == null) {
return;
}
boolean flag;
for (int i = arr.length - 1; i > 0; i--) {
flag = false;
for (int j = 0; j < i; j++) {
if (arr[j] > arr[j + 1]) {
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
flag = true;
}
}
if (!flag) {
return;
}
}
}
快速排序
快速排序是由冒泡排序改进而得到的,是一种排序执行效率很高的排序算法,它利用分治法来对待排序 序列进行分治排序,它的思想主要是通过一趟排序将待排记录分隔成独立的两部分,其中的一部分比关 键字小,后面一部分比关键字大,然后再对这前后的两部分分别采用这种方式进行排序,通过递归的运 算最终达到整个序列有序。
快速排序的过程如下:
- 在待排序的N个记录中任取一个元素(通常取第一个记录)作为基准,称为基准记录;
- 定义两个索引 left 和 right 分别表示首索引和尾索引,key 表示基准值; 3. 首先,尾索引向前扫描,直到找到比基准值小的记录,并替换首索引对应的值;
- 然后,首索引向后扫描,直到找到比基准值大于的记录,并替换尾索引对应的值;
- 若在扫描过程中首索引等于尾索引(left = right),则一趟排序结束;将基准值(key)替换首索引所对 应的值;
- 再进行下一趟排序时,待排序列被分成两个区:[0,left-1]和[righ+1,end] 7. 对每一个分区重复以上步骤,直到所有分区中的记录都有序,排序完成
快排为什么比冒泡效率高? 快速排序之所以比较快,是因为相比冒泡排序,每次的交换都是跳跃式的,每次设置一个基准值,将小 于基准值的都交换到左边,大于基准值的都交换到右边,这样不会像冒泡一样每次都只交换相邻的两个 数,因此比较和交换的此数都变少了,速度自然更高。 快速排序的平均时间复杂度是O(nlgn),最坏时间复杂度是O(n^2)。
代码实现、
public void quickSort(int[] arr) {
if (arr == null) {
return;
}
quickSortHelper(arr, 0, arr.length - 1);
}
private void quickSortHelper(int[] arr, int left, int right) {
if (left > right) {
return;
}
int tmp = arr[left];
int i = left;
int j = right;
while (i < j) {
//j先走,最终循环终止时,j停留的位置就是arr[left]的正确位置
//改为i<=j,则会进入死循环,[1,5,5,5,5]->[1] 5 [5,5,5]->[5,5,5],会死循环
while (i < j && arr[j] >= tmp) {
j--;
}
while (i < j && arr[i] <= tmp) {
i++;
}
if (i < j) {
int tmp1 = arr[i];
arr[i] = arr[j];
arr[j] = tmp1;
} else {
break;
}
}
//当循环终止的时候,i=j,因为是j先走的,j所在位置的值小于arr[left],交换arr[j]和
arr[left]
arr[left] = arr[j];
arr[j] = tmp;
quickSortHelper(arr, left, j - 1);
quickSortHelper(arr, j + 1, right);
}
插入排序
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
算法描述:
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
代码实现
public void insertSort(int[] arr) {
if (arr == null) {
return;
}
for (int i = 1; i < arr.length; i++) {
int tmp = arr[i];
int j = i;
for (; j > 0 && tmp < arr[j - 1]; j--) {
arr[j] = arr[j - 1];
}
arr[j] = tmp;
}
}
选择排序
表现最稳定的排序算法之一,因为无论什么数据进去都是O(n2)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间。
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
思路:n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:
- 初始状态:无序区为R[1..n],有序区为空;
- 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无
- 序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
- n-1趟结束,数组有序化了。
代码实现:
希尔排序
基数排序
基数排序也是非比较的排序算法,对每一位进行排序,从最低位开始排序,复杂度为O(kn),为数组长
度,k为数组中的数的最大的位数;
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时
候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的
在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。
算法描述:
- 取得数组中的最大数,并取得位数;
- arr为原始数组,从最低位开始取每个位组成radix数组;
- 对radix进行计数排序(利用计数排序适用于小范围数的特点);
代码实现:
public static int[] RadixSort(int[] array) {
if (array == null || array.length < 2)
return array;
// 1.先算出最大数的位数;
int max = array[0];
for (int i = 1; i < array.length; i++) {
max = Math.max(max, array[i]);
}
int maxDigit = 0;
while (max != 0) {
max /= 10;
maxDigit++;
}
int mod = 10, div = 1;
ArrayList<ArrayList<Integer>> bucketList = new ArrayList<ArrayList<Integer>>();
for (int i = 0; i < 10; i++)
bucketList.add(new ArrayList<Integer>());
for (int i = 0; i < maxDigit; i++, mod *= 10, div *= 10) {
for (int j = 0; j < array.length; j++) {
int num = (array[j] % mod) / div;
bucketList.get(num).add(array[j]);
}
int index = 0;
for (int j = 0; j < bucketList.size(); j++) {
for (int k = 0; k < bucketList.get(j).size(); k++)
array[index++] = bucketList.get(j).get(k);
bucketList.get(j).clear();
}
}
return array;
}
计数排序
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度 的排序,计数排序要求输入的数据必须是有确定范围的整数。
计数排序(Counting sort)是一种稳定的排序算法。计数排序使用一个额外的数组C,其中第i个元素是待 排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。它只能对整数进行 排序。
代码实现
public static int[] CountingSort(int[] array) {
if (array.length == 0) return array;
int bias, min = array[0], max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max)
max = array[i];
if (array[i] < min)
min = array[i];
}
bias = 0 - min;
int[] bucket = new int[max - min + 1];
Arrays.fill(bucket, 0);
for (int i = 0; i < array.length; i++) {
bucket[array[i] + bias]++;
}
int index = 0, i = 0;
while (index < array.length) {
if (bucket[i] != 0) {
array[index] = i - bias;
bucket[i]--;
index++;
} else
i++;
}
return array;
}
归并排序
归并排序 (merge sort) 是一类与插入排序、交换排序、选择排序不同的另一种排序方法。归并的含义是 将两个或两个以上的有序表合并成一个新的有序表。归并排序有多路归并排序、两路归并排序 , 可用于 内排序,也可以用于外排序。
两路归并排序算法思路是递归处理。每个递归过程涉及三个步骤
- 分解: 把待排序的 n 个元素的序列分解成两个子序列, 每个子序列包括 n/2 个元素
- 治理: 对每个子序列分别调用归并排序MergeSort, 进行递归操作
- 合并: 合并两个排好序的子序列,生成排序结果
时间复杂度:对长度为n的序列,需进行logn次二路归并,每次归并的时间为O(n),故时间复杂度是 O(nlgn)。
空间复杂度:归并排序需要辅助空间来暂存两个有序子序列归并的结果,故其辅助空间复杂度为O(n)
代码实现
public class MergeSort {
public void mergeSort(int[] arr) {
if (arr == null || arr.length == 0) {
return;
}
//辅助数组
int[] tmpArr = new int[arr.length];
mergeSort(arr, tmpArr, 0, arr.length - 1);
}
private void mergeSort(int[] arr, int[] tmpArr, int left, int right) {
if (left < right) {
int mid = (left + right) >> 1;
mergeSort(arr, tmpArr, left, mid);
mergeSort(arr, tmpArr, mid + 1, right);
merge(arr, tmpArr, left, mid, right);
}
}
private void merge(int[] arr, int[] tmpArr, int left, int mid, int right) {
int i = left;
int j = mid + 1;
int tmpIndex = left;
while (i <= mid && j <= right) {
if (arr[i] < arr[j]) {
tmpArr[tmpIndex++] = arr[i];
i++;
} else {
tmpArr[tmpIndex++] = arr[j];
j++;
}
}
while (i <= mid) {
tmpArr[tmpIndex++] = arr[i++];
}
while (j <= right) {
tmpArr[tmpIndex++] = arr[j++];
}
for (int m = left; m <= right; m++) {
arr[m] = tmpArr[m];
}
}
}
堆排序
堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者 每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
Top大问题解决思路:使用一个固定大小的最小堆,当堆满后,每次添加数据的时候与堆顶元素比较, 若小于堆顶元素,则舍弃,若大于堆顶元素,则删除堆顶元素,添加新增元素,对堆进行重新排序。
对于n个数,取Top m个数,时间复杂度为O(nlogm),这样在n较大情况下,是优于nlogn(其他排序算 法)的时间复杂度的。
PriorityQueue 是一种基于优先级堆的优先级队列。每次从队列中取出的是具有最高优先权的元素。如果不提供Comparator的话,优先队列中元素默认按自然顺序排列,也就是数字默认是小的在队列头。 优先级队列用数组实现,但是数组大小可以动态增加,容量无限。
代码实现
//找出前k个最大数,采用小顶堆实现
public static int[] findKMax(int[] nums, int k) {
//队列默认自然顺序排列,小顶堆,不必重写compare
PriorityQueue<Integer> pq = new PriorityQueue<>(k);
for (int num : nums) {
if (pq.size() < k) {
pq.offer(num);
} else if (pq.peek() < num) {
//如果堆顶元素 < 新数,则删除堆顶,加入新数入堆
pq.poll();
pq.offer(num);
}
}
int[] result = new int[k];
for (int i = 0; i < k&&!pq.isEmpty(); i++) {
result[i] = pq.poll();
}
return result;
}