排序算法是算法题中非常常见的一类题,相关的排序方法也很多,如:快速排序、冒泡排序、归并排序、简单选择排序、堆排序等等。本文以Leetcode 面试题 17.14. 最小K个数为例,对以上排序算法做一个回顾。
冒泡排序
冒泡排序是一种交换排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
算法步骤:
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;
- 重复步骤1~3,直到排序完成。
public int[] smallestK(int[] arr, int k) {
int len=arr.length;
for(int i=0;i<len-1;i++){
for(int j=0;j<len-i-1;j++)
if (arr[j+1]<arr[j]){
int tmp=arr[j+1];
arr[j+1]=arr[j];
arr[j]=tmp;
}
}
int[] res=new int[k];
for(int i=0;i<k;i++){
res[i]=arr[i];
}
return res;
}
快速排序
思路:
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
//将数组完全快排
public static int[] smallestK(int[] arr, int k) {
if (k >= arr.length) {
return arr;
}
int low = 0;
int high = arr.length - 1;
sort(arr,low,high);
int[] dest = new int[k];
//arr是要复制的数组,第一个0是源数组被复制的起始位置,dest是目标数组,第二个0是目标数组开始复制的位置,k代表要复制的数组的长度
System.arraycopy(arr, 0, dest, 0, k);
return dest;
}
public static void sort(int []arr, int left, int right) {
int privot;
if(left<right) {
privot = partition(arr, left, right);
sort(arr, left, privot-1);
sort(arr, privot+1, right);
}
}
private static int partition(int[] arr, int low, int high) {
int povit=arr[low];
while (low<high){
while (low<high&&arr[high]>=povit){
high--;
}
arr[low]=arr[high];
while (low<high&&arr[low]<=povit){
low++;
}
arr[high]=arr[low];
}
arr[low]=povit;
return low;
}
实际上,上面的代码有更简单的写法,已知要输出前k小的元素,所以,只需要将前k小的输出即可,所以代码可以改为:
public static int[] smallestK(int[] arr, int k) {
if (k >= arr.length) {
return arr;
}
int low = 0;
int high = arr.length - 1;
while (low < high) {
int pos = partition(arr, low, high);
if (pos == k - 1) {
low = pos + 1;
} else if (pos < k - 1) {
low = pos + 1;
} else {
high = pos - 1;
}
}
int[] dest = new int[k];
//arr是要复制的数组,第一个0是源数组被复制的起始位置,dest是目标数组,第二个0是目标数组开始复制的位置,k代表要复制的数组的长度
System.arraycopy(arr, 0, dest, 0, k);
return dest;
}
private static int partition(int[] arr, int low, int high) {
int povit=arr[low];
while (low<high){
while (low<high&&arr[high]>=povit){
high--;
}
arr[low]=arr[high];
while (low<high&&arr[low]<=povit){
low++;
}
arr[high]=arr[low];
}
arr[low]=povit;
return low;
}
简单插入排序
public static int[] smallestK(int[] arr, int k) {
int len=arr.length;
for (int i=1;i<len;i++){
int j=i-1;
int tmp=arr[i];
while (j>=0&&tmp<arr[j]){
arr[j+1]=arr[j];
j--;
}
arr[j+1]=tmp;
}
int[] res=new int[k];
for (int i=0;i<k;i++){
res[i]=arr[i];
}
return res;
}
希尔排序
思想:将整个数组以gap为间隔划分为若干个组,在若干个组内分别进行插入排序。然后降低gap,重复上面的步骤。到gap=1时,即为插入排序。
具体步骤:
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 按增量序列个数k,对序列进行k 趟排序;
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
public static int[] smallestK(int[] arr, int k) {
int len=arr.length;
int gap=len/2;
while (gap>0){
for (int num=gap;num<len;num++){
int tmp=arr[num];
int j;
for (j=num;j-gap>=0&&tmp<arr[j-gap];j-=gap){
arr[j]=arr[j-gap];
}
arr[j]=tmp;
}
gap/=2;
}
int[] res=new int[k];
for (int i=0;i<k;i++){
res[i]=arr[i];
}
return res;
}