作为计算机面试最基础的算法,今天来整理复习一下数据结构中的八大排序算法,面试前必须熟练到可以在纸上手写出来,不依靠IDE。
算法复杂度对比
插入排序
直接插入排序
某一时刻待排序列表状态如下:
有序序列L[1…i-1] | L[i] | 无序序列L[i+1….n] |
---|
需要进行以下操作:
1)查找出L[i]在L[1…i-1]中的插入位置k
2)将L[k…i-1]中所有元素全部后移一个位置
3)将L[i]复制到L[k]
执行n-1次就能得到一个有序的表。
使用常数个辅助单元,空间复杂度为o(1)
时间复杂度为o(n2)
由于每次都是从后向前比较后才移动,不会出现相同元素相对位置发生变化的情况。
public class InsertSort {
public static void main(String[] args) {
int[] array = {4,7,2,1,5,3,8,6};
int i,j;
for (i = 1; i < array.length; i++) {
int tmp = array[i];
for (j = i-1; j >= 0; j--) {//注意这个循环
if (tmp < array[j]){//一边比较一边后移
array[j+1] = array[j];
}else{
break;
}
}
array[j+1] = tmp;
}
for (int k = 0; k < array.length; k++) {
System.out.print(array[k]+" ");
}
}
}
希尔排序
希尔排序也称为“缩小增量排序”,基本原理是:首先将待排序的元素分为多个子序列,使得每个子序的元素个数相对较少,对各个子序分别进行直接插入排序,待整个待排序序列“基本有序后”,再对所有元素进行一次直接插入排序。
先取一个小于n的整数d1作为第一个增量,把文件的全部记录分组。
所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;
然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量 dt =1( dt < …<d2<d1),即所有记录放在同一组中进行直接插入排序为止。
最坏情况下希尔排序的时间复杂度为o(n2)。
由于多次插入排序,元素可能在不同的插入排序中改变相对位置,所以希尔排序是不稳定的排序算法。
目前尚未求得一个最好的增量序列,希尔提出的方法是d1=n/2,di+1 = di/2(向下取整),最后一个增量等于1。
注意实现的时候跟逻辑上的有些许不同,例:
第一趟排序只会排49和13,并不会49,13,19一起排,排完49和13就排38和27。
public class ShellSort {
public static void main(String[] args) {
int[] array = {4,7,2,1,5,3,8,6};
int i,j,d;
for (d = array.length/2; d > 0 ; d /= 2) {
for (i = d; i < array.length; i++) {//对每小段进行插入排序
int tmp = array[i];
for (j = i - d; j >= 0 ; j -= d) {
if (array[j] > tmp)
array[i] = array[j];
else
break;
}
array[j+d] = tmp;
}
}
for (int k = 0; k < array.length; k++) {
System.out.print(array[k]+" ");
}
}
}
交换排序
冒泡排序
冒泡排序的基本思想:从前往后(或从后往前)两两比较相邻元素的值,若为逆序则交换,每一趟都会将一个最大(小)元素放到最终位置。最多重复n-1趟。
时间复杂度为o(n2)
只用常数个辅助单元,空间复杂度为o(1)
A[i]=A[i+1]时不会交换两个元素,所以冒泡排序是稳定的
public class BubbleSort {
public static void main(String[] args) {
int[] array = {4,7,2,1,5,3,8,6};
int i,j;
boolean flag;//用来检测本趟有没有发生交换,若没有则已完成排序
for (i = 0; i < array.length-1; i++){
flag = false;
for (j = 0; j < array.length-1; j++)
if(array[j] > array[j+1]){
swap(array,j);
flag = true;//若发生交换,置flag为true
}
if(flag == false)//若这趟没有发生交换,说明已全部有序,后面不用再循环了
break;
}
for (int k = 0; k < array.length; k++) {
System.out.print(array[k]+" ");
}
}
private static void swap(int[] array, int j) {
int tmp = array[j+1];
array[j+1] = array[j];
array[j] = tmp;
}
}
快速排序
快速排序的基本思想是分而治之,选择一个pivot座位基准,通过一趟排序将表划分为独立的两部分L[1….k-1]和L[k+1……n],L[1….k-1]中所有元素小于pivot,L[k+1……n]中所有元素大于等于pivot,则pivot放在最终位置上。递归这个过程直至每个部分只有1个元素为止。
快速排序用到递归,递归需要用到栈,平均情况下空间复杂度为o(log2n),最坏情况下空间复杂度为o(n)
当初始表基本有序或者基本逆序时,快速排序将退化为冒泡排序,得到最坏的时间性能o(n2),而其平均性能接近最好性能 o(nlog2n)。快排是所有内部排序算法中平均性能最优的排序算法。
public class QuickSort {
public static void main(String[] args) {
int[] array = {4,7,2,1,5,3,8,6};
quicksort(array,0,array.length-1);
for (int k = 0; k < array.length; k++) {
System.out.print(array[k]+" ");
}
}
private static void quicksort(int[] array, int low, int high) {
if(low < high){
int pivot = partition(array,low,high);
quicksort(array,low,pivot-1);
quicksort(array,pivot+1,high);
}
}
//划分操作
private static int partition(int[] array, int low, int high) {
int pivot = array[low];
while (low < high){
//因为我们让low作为pivot,所以要从high开始。
while (low < high && array[high] >= pivot) high--;//定位到比pivot小的位置
array[low] = array[high];
while (low < high && array[low] <= pivot) low++;
array[high] = array[low];
}
array[low] =pivot;
return low;
}
}
选择排序
简单选择排序
对于给定的一组记录,经过第一轮比较后得到最小的记录,然后将记录与第一个记录的位置进行交换;接着对不包括第一个记录以外的其他记录进行第二轮排序,得到最小的记录并与第二个记录进行位置交换;重复该过程,直到进行比较的记录只有一个为止。
时间复杂度为o(n2) 不稳定
public class SelectSort {
public static void main(String[] args) {
int[] array = {4,7,2,1,5,3,8,6};
int min;
for (int i = 0; i < array.length; i++) {
min = i;
for (int j = i+1; j < array.length; j++) {
if (array[min] > array[j])
min = j;
}
if (min != i){
int tmp = array[i];
array[i] = array[min];
array[min] = tmp;
}
}
for (int k = 0; k < array.length; k++) {
System.out.print(array[k]+" ");
}
}
}
堆排序
将序列构造成一棵完全二叉树 ;
把这棵普通的完全二叉树改造成堆,便可获取最小值 ;
输出最小值 ;
删除根结点,继续改造剩余树成堆,便可获取次小值 ;
输出次小值 ;
重复改造,输出次次小值、次次次小值,直至所有结点均输出,便得到一个排序 。
时间复杂度为o(nlog2n) 不稳定
public class HeapSort {
public static void main(String[] args) {
int[] array = {4,7,2,1,5,3,8,6};
//构造初始堆,指针是从0开始的,所以是len/2-1
for (int i = array.length/2-1; i >= 0; i--) {
AdjustDown(array,i,array.length);
}
for (int i = array.length-1; i > 0 ; i--) {
//交换堆顶和最后一个元素,重新调整堆
swap(array,i,0);
AdjustDown(array,0,i-1);
}
for (int k = array.length-1; k >= 0; k--) {
System.out.print(array[k]+" ");
}
}
private static void swap(int[] array, int i, int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
//最核心部分 调整形成小顶堆 i是工作指针
private static void AdjustDown(int[] array, int i, int len) {
int tmp = array[i];
for (int j = i * 2 + 1; j < len ; j = j*2+1) {//注意j的步长是*2+1
if (j + 1 < len && array[j] > array[j+1])
j++;//选择较小的子节点
if (tmp > array[j]){//如果交换了,子树会受影响,所以循环判断
array[i] = array[j];
i = j;
}
else //没有发生交换,不用再循环。
break;
}
array[i] = tmp;
}
}
这个排序算法也是调代码调到最吐血的一个算法,个人认为在八个排序算法里面属最难。
基数排序
基数排序不是基于比较进行排序的,而是基于关键字各位的大小进行排序的。包括“分配”和“收集”两种操作。(基数排序是一种特殊的桶排序)
假设排序的数中最大的是d位数,基数排序需要进行d趟分配和收集,所以基数排序的时间复杂度是o(d*n)
import java.util.Vector;
public class RadixSort {
public static void main(String[] args) {
int[] a = {278,109,63,930,589,184,505,269,8,83};
int d = maxd(a);
int i = 1;//从个位开始
int tmpi = 0;
int[] num ={0,0,0,0,0,0,0,0,0,0};//每个桶里面装的数字的个数
int[][] buckets = new int[10][a.length];//第一维是0~10
for (int j = 0; j < d; j++) {
//分配
for (int x:a){
int tmp = ((x/i)%10);
buckets[tmp][num[tmp]] = x;
num[tmp]++;
}
//收集
for (int k = 0; k < 10; k++) {
if(num[k] != 0)
for (int l = 0; l < num[k]; l++) {
a[tmpi++] = buckets[k][l];
}
num[k] = 0;//清空桶
}
tmpi = 0;//记得重置
i *= 10;
}
for (int x:a) {
System.out.print(x+" ");
}
}
//求出数组中的数据最大是几位数
private static int maxd(int[] a) {
int max = 0;
for (int x:a) {
int d = 0;
while (x > 0){
x /= 10;
d++;
}
if (d > max)
max = d;
}
return max;
}
}
归并排序
假定待排序表含有n个记录,则可以看成是n个有序的子表,每个子表长为1,然后两两归并,得到n/2个长度为2或1的有序表,再两两归并,。。。。如此重复,直到合并成一个长度为n的有序表为止。这种排序成为2路归并排序。
归并排序是一种稳定的排序算法。
每趟归并的时间复杂度为o(n),共需进行log2n(向下取整)趟,所以时间复杂度为o(nlog2n)
辅助数组刚好要使用n个单元,所以空间复杂度为o(n)
public class MergeSort {
public static void main(String[] args) {
int[] a = {4,7,2,1,5,3,8,6};
mergeSort(a,0,a.length-1);
for (int x:a) {
System.out.print(x+" ");
}
}
private static void mergeSort(int[] a, int low, int high) {
if (low < high){
int mid = (low + high) / 2;
mergeSort(a,low,mid);//对左侧子序列进行递归排序
mergeSort(a,mid+1,high);//对右侧子序列进行递归排序
Merge(a,low,mid,high);//归并
}
}
private static void Merge(int[] a, int low, int mid, int high) {
int[] tmp = new int[a.length];
int j = low;
int k = mid+1;
int l = j;//l是要在数据a中用的指针
//将a中的数据复制到辅助表中
for (int i = low; i <= high; i++)
tmp[i] = a[i];
for (j = low; j <= mid && k <= high; l++) {//注意看好这里是l++
if (tmp[j] <= tmp[k])
a[l] = tmp[j++];//j和k的指针也要记得++
else
a[l] = tmp[k++];
}
//把子表中剩余的数复制到数组中
while (j <= mid)
a[l++] = tmp[j++];
while (k <= high)
a[l++] = tmp[k++];
}
}