冒泡排序
基本思想:两两相邻进行比较,将最大的放到后面,每轮比较都会使需要比较的元素数目减一,总共需要比较length-1轮。由于嵌套for循环,时间复杂度最优和平均都是o(n^2),空间复杂度o(1)。冒泡排序是稳定的。
public class Main {
public static void bubblingSort(int[] list) {
int i, j;
int length = list.length;
for (i = length - 1; i > 0; i--) {
for (j = 0; j < i; j++) {
if (list[j] > list[j + 1])
swap(list, j, j + 1);
}
}
}
/**
* 打印
* @param list
*/
private static void show(int[] list) {
for (int i : list) {
System.out.print(i + " ");
}
System.out.println();
}
/**
* 交换
* @param list
* @param a
* @param b
*/
public static void swap(int[] list, int a, int b) {
int temp = list[a];
list[a] = list[b];
list[b] = temp;
}
public static void main(String[] args) {
int[] list = new int[] { 2, 17, 5, 1, 2, 8, 4, 13, 11, 6 };
bubblingSort(list);
show(list);
}
}
优化点:如果数组为[3,2,4,5],其实在执行第一轮后,[2,3,4,5],数组已经有序,为了避免数组有序情况下继续执行循环,可以设定一个信号flag,改进代码如下
public static void bubblingSort(int[] list) {
int i, j;
int length = list.length;
boolean flag=true;//设定信号量,初始化为true
for (i = length - 1; i > 0&&flag; i--) {
flag=false;//每一轮都将信号量置为flase
for (j = 0; j < i; j++) {
if (list[j] > list[j + 1]){
swap(list, j, j + 1);
flag=true;//该轮发生交换,继续执行下一轮;否则数组已经有序,跳出循环
}
}
}
}
选择排序
基本思想:每轮选出最小的元素与每轮第一个元素交换
{5,4,1,3} -> {1,4,5,3} -> {1,3,5,4} -> {1,3,4,5}
平均、最好、最坏的时间复杂度都是o(n^2),空间复杂度o(1)。选择排序不是稳定的如{5,5,2}。
public static void choiceSort(int[] list) {
int i, j;
int length = list.length;
int temp;//记录每一轮最小值
int index;//记录每一轮最小值下标
for(i=0;i<length-1;i++){
temp=list[i];
index=i;
for(j=i+1;j<length;j++){
if(list[j]<temp){
//更新最小值
temp=list[j];
index=j;
}
}
//将最小值与该轮的第一个元素交换
swap(list,i,index);
}
}
插入排序
基本思想:玩扑克牌,每次抓取一张牌,都将它插入到手牌合适的位置。
原数组:{5,4,1,3}
排序:{5} -> {4,5} -> {1,4,5} -> {1,3,4,5}
最好情况时间复杂度:o(n),如{1,2,3,4},只需要遍历(n-1)次。平均、最坏均是o(n^2)。空间复杂度:o(1)。插入排序是稳定的。
public static void insertSort(int[] list) {
int i, j;
int length = list.length;
int temp;//记录抽取卡牌的值
//从第二张牌开始插入
for(i=1;i<length;i++){
temp=list[i];
//将抽取的牌与手牌逐一比较,找到合适位置,大于抽取卡牌值的手牌需要向后移一步
for(j=i;j>0&&temp<list[j-1];j--){
list[j]=list[j-1];
}
list[j]=temp;//插入合适位置
}
}
希尔排序
基本思想:其实希尔排序是插入排序的一种优化,插入排序中提及到,当所需要排序的数组是有序的(默认为升序)情况下,达到最优时间复杂度。希尔排序就是借助这个特性进行优化,设置一个增量值gap,先粗略地对数组进行插入排序,通过不断缩小gap的值,继续对数组进行细化排序,由于每次都是基于前一次排序的结果进行插入排序,使得比较次数减小。借用dreamcatcher-cx博主的图:
public static void shellSort(int[] list) {
int i, j;
int length = list.length;
int temp;//插入的卡牌
int gap;//增量值
for(gap=length/2;gap>0;gap/=2){
for(i=gap;i<length;i++){
//流程和插入排序差不多
temp=list[i];
//注意循环出口j-gap>=0,否则在判断temp<list[j-gap]会报数组越界错误
for(j=i;j-gap>=0&&temp<list[j-gap];j-=gap){
list[j]=list[j-gap];
}
list[j]=temp;
}
}
}
值得注意的是,希尔排序并不像直接插入排序,它是不稳定的,例如{4,3,3,2},当gap=2,排序得:{3,2,4,3};当gap=1,排序得{2,3,3,4}。另外,希尔排序的平均时间复杂度也是不确定的o(nlogn)~o(n^2),最好情况o(n^1.3),最坏情况o(n^2)。空间复杂度o(1)。
快速排序
基本思想:【坐在马桶上看算法】算法3:最常用的排序——快速排序
时间复杂度:平均o(nlogn),最优o(nlogn),最坏o(n^2)如{4,3,2,1}
空间复杂度:o(logn)~o(n),主要因为使用递归会占用栈空间
稳定性:不稳定
public static void sort(int[] list,int left,int right) {
if(left>=right) return;//递归出口
//找到中点
int mid=execute(list,left,right);
//左右分支
sort(list,left,mid-1);
sort(list,mid+1,right);
}
public static int execute(int[] list,int left,int right){
int base=list[left];//设置基数,这里取最左元素,会可能导致出现最坏情况,后续优化
int index=left;//记录基数下标
while(left<right){
//右指针向左移动,找到比基数值小的元素(为什么右指针先行?)
while(left<right){
if(list[right]<base) break;
right--;
}
//左指针向右移动,找到比基数大的元素
while(left<right){
if(list[left]>base) break;
left++;
}
if(left<right){
swap(list,left,right);//左右指针值交换
}
}
swap(list,index,left);//基数与中间值交换
return left;//返回中间值下标
}
这里有两个问题,首先解决为什么右指针先移动,我们假设现在处于这种情况:
选取5为基数
如果左指针先行,找到7>5
然后右指针向左移动,right==left,跳出循环,执行swap(list,index,left)。
将基数和中间值交换后,发现基数5的左边明显不是所有数都小于5,排序出现错误。
那么第二个问题,为什么选取最左或者最右元素作为基数会可能导致最坏情况出现?继续举个栗子:
快排的基本步骤
if(left>=right) return;//递归出口
//找到中点
int mid=execute(list,left,right);
//右分支
sort(list,mid+1,right);
//左分支
sort(list,left,mid-1);
假设初始数组{4,3,2,1}
第一次mid返回3,进入右分支sort(list,4,3)
left>right退出递归,进入左分支sort(list,0,2)
第二次mid返回0,进入右分支sort(list,1,2)
,进入左分支sort(list,0,-1)
left < right退出递归
第三次mid返回2,进入右分支sort(list,3,2)
退出递归,进入左分支sort(list,1,1)
退出递归。第三次结束后数组排序完成,观察发现,总共执行n-1次。由于每次调用递归,栈深度加一,可以得出空间复杂度o(n),而每次左指针需要遍历到最右边或者右指针需要遍历到最左边,所以时间复杂度为o(n^2)。对于进一步的优化,可参考快速排序及优化(Java实现)
归并排序
基本思想:递归分治,对数组不断地对半拆分,然后再重新排序合并,如图(图片来源)
时间复杂度:平均、最好、最坏都是o(nlogn)。
空间复杂度:o(n),借助临时数组。
稳定性:稳定。
public static void mergeSort(int[] list,int left,int right) {
if(left>=right) return;//递归出口
//找到中点
int mid=(left+right)/2;
//左右分支
mergeSort(list,left,mid);
mergeSort(list,mid+1,right);
//归并
execute(list,mid,left,right );
}
public static void execute(int[] list,int mid,int left,int right){
//临时数组
int[] temp=new int[right-left+1];
int i=left,j=mid+1;
int k=0;
while(i<=mid&&j<=right){
//左右两边比较,较小的先放入数组
if(list[i]<list[j]){
temp[k++]=list[i++];
}else{
temp[k++]=list[j++];
}
}
//将剩余的放入temp
while(i<=mid) temp[k++]=list[i++];
while(j<=right) temp[k++]=list[j++];
//将临时数组值放入list
for(int t=0;t<temp.length;t++){
//注意放入list的起始下标应该从left开始
list[left+t]=temp[t];
}
}
堆排序
基本思想:构建一个堆, 使得根节点值最大,将根节点与堆最后一个元素交换,重新构建堆,堆大小减一,反复执行。
时间复杂度:o(nlogn)。
空间复杂度:o(1)。
稳定性:不稳定。
public static void heapSort(int[] list) {
//建堆,从最后一个父节点开始,由下至上构建堆
for(int i=list.length/2-1;i>=0;i--){
adjust(list,list.length,i);
}
//排序
for(int i=list.length-1;i>0;i--){
//堆顶和数组最后一个元素交换
swap(list,0,i);
//调整堆
adjust(list,i,0);
}
}
/**
* 构建或者调整堆
* @param list
* @param size 堆大小
* @param i 当前父节点
*/
public static void adjust(int[] list,int size,int i){
//左右节点
int left=i*2+1;
int right=i*2+2;
//记录左右节点最小值下标
int maxindex=i;
if(left<size&&list[left]>list[maxindex]) maxindex=left;
if(right<size&&list[right]>list[maxindex]) maxindex=right;
//minIndex未发生改变,即当前父节点最小,返回
if(maxindex==i) return;
//交换父子节点,重新构建堆
swap(list,i,maxindex);
adjust(list,size,maxindex);
}