4排序算法

排序算法

1冒泡排序

​ 1.1概念

  • 冒泡排序(bubble sort)是一种基础的交换排序
  • 思想:我们要把相邻的元素两两比较,当一个元素大于右侧元素时,交换它们的顺序;当一个元素小于或等于右侧相邻元素时,位置不变
  • 冒泡排序是一种稳定排序,平均时间复杂度是O(n^2)

​ 1.2冒泡排序第一版

 public static void sort(int array[]){
        for (int i=0;i<array.length-1;i++){
            for (int j=0;j<array.length-i-1;j++){
                int temp=0;
                if (array[j]>array[j+1]){
                    temp=array[j];
                    array[j]=array[j+1];
                    array[j+1]=temp;
                }
            }
        }
    }

​ 1.3冒泡排序第二版

  • 一轮中不交换了就直接停止循环
  • 按照上面的逻辑,有序区的长度和排序的轮数是相等的,但实际上,数列真正的有序区可能会大于这个长度
 public static void sort(int array[]){
        for (int i=0;i<array.length-1;i++){
            //有序标记,每一轮的初始值都是true
            boolean isSorted=true;
            //无序数列的边界,每次比较只需要比到这里为止
            int sortBorder=array.length-1;
            for (int j=0;j<sortBorder;j++){
                int temp=0;
                if (array[j]>array[j+1]){
                    temp=array[j];
                    array[j]=array[j+1];
                    array[j+1]=temp;
                    //因为有元素进行交换,所以不是有序的,标记为false
                    isSorted=false;
                    //把无序数列的边界更新为最后一次交换元素的位置
                    sortBorder=j;
                }
            }
            if (isSorted){
                break;
            }
        }
    }

​ 1.4鸡尾酒排序

  • 冒泡排序每一轮都是从左到右来比较元素,进行单向的位置交换
  • 鸡尾酒排序的元素比较过程和交换过程都是双向的
public static void sort(int array[]){
        int temp=0;
        for (int i=0;i<array.length/2;i++){
            //有序标记,每一轮的初始值都是true
            boolean isSorted =true;
            //奇数轮,从左向右比较和交换
            for (int j=i;j<array.length-i-1;j++){
                if (array[j]>array[j+1]){
                    temp=array[j];
                    array[j]=array[j+1];
                    array[j+1]=temp;
                    isSorted=false;
                }
            }
            if (isSorted){
                break;
            }
            //在偶数轮之前,将isSorted重新标志为true
            isSorted=true;
            //偶数轮,从右向左比较和交换
            for (int j=array.length-i-1;j<i;j--){
                if (array[j]<array[j-1]){
                    temp=array[j];
                    array[j]=array[j-1];
                    array[j-1]=temp;
                    isSorted=false;
                }
            }
            if (isSorted){
                break;
            }
        }
        
    }
  • 鸡尾酒排序优点是能在特定条件下,减少排序的回合数,缺点就是代码量几乎增加了一倍
  • 它能发挥优势的场景就是大部分元素以及有序的情况

2选择排序

​ 2.1概述

  • 每一轮选出最小元素交换到最左侧的思路就是选择排序
  • 时间复杂度为O(n^2),空间复杂度为O(1),是不稳定的

​ 2.2代码实现

 public static void selectionSort(int[] array){
       for (int i=0;i<array.length;i++){
           int minIndex=i;
           for (int j=i;j<array.length;j++){
               if (array[j]<array[minIndex]){
                   minIndex=j;
               }
           }
           if (i!=minIndex){
               int temp=array[i];
               array[i]=array[minIndex];
               array[minIndex]=temp;
           }
       }
   }

3插入排序

​ 3.1概述

  • 具体思路和扑克牌一样,在有序数组中插入元素
  • 优化:每次插入时,不需要进行完整的交换,只需要把要插入元素暂时存起来,再把有序元素从前向后逐一复制

​ 3.2代码

 public static void insertSort(int[] array){
        for (int i=1;i<array.length;i++){
            int insertValue=array[i];
            int j=i-1;
            //从右向左比较元素的同时,进行元素复制
            for (;(j>=0)&&(insertValue<array[j]);j--){
                array[j+1]=array[j];
            }
            //insertValue的值插入适当位置
            array[j+1]=insertValue;
        }
    }

4快速排序

​ 4.1概述

  • 快速排序也属于交换排序
  • 思想:在每一轮挑选一个基准元素,并让其他比它的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成两个部分
  • 总体时间复杂度是O(nlogn),空间复杂度是O(logn),是不稳定的

​ 4.2基准元素的选择

  • 基准元素pivot,在分治过程中,以基准元素为中心,把其他元素移动到它的左右两边
  • 最简单的方式就是选择数列的第一个元素
  • 这样如果数列本身逆序就会形成极端情况,时间复杂读就变成O(n^2)
  • 更好的是我们随机选择一个元素作为基准元素,并且让基准元素和数列首元素交换位置

​ 4.3代码实现

  • 单边循环法(选定基准元素pivot,同时设置一个mark指针指向数列起始位置,这个mark指针代表小于基准元素的区域边界)
  • 双边循环法
 public static void quickSort(int[] arr,int startIndex,int endIndex){
        //递归结束条件:startIndex大于或等于endIndex
        if (startIndex>=endIndex){
            return;
        }
        //得到基准元素位置
        int pivotIndex=partition(arr,startIndex,endIndex);
        quickSort(arr,startIndex,pivotIndex-1);
        quickSort(arr,pivotIndex+1,endIndex);
    }
    
    //分治,双边循环法
    private static int partition(int[] arr,int startIndex,int endIndex){
        //取第一个位置,(也可以选择随机位置)的元素作为基准元素
        int pivot=arr[startIndex];
        int left=startIndex;
        int right=endIndex;
        
        while (left!=right){
            //控制right指针比较并左移
            while (left<right && arr[right]>pivot){
                right--;
            }
            //控制left指针比较并右移
            while (left<right && arr[left]<=pivot){
                left++;
            }
            //交换left和right指针所指向的元素
            if (left<right){
                int temp=arr[left];
                arr[left]=arr[right];
                arr[right]=temp;
            }
        }
        //pivot和指针重合点交换
        arr[startIndex]=arr[left];
        arr[left]=pivot;
        return left;
    }

5希尔排序

​ 5.1思想

  • 逐步分组进行粗调,再进行直接插入排序的思路,就是希尔排序
  • 比如8个元素,采用逐步折半的增量方法,分组跨度就是4,2,1,这种也被称为希尔增量

​ 5.2代码

public static void shellSort(int[] array){
       //希尔排序的增量
       int d=array.length;
       while (d>1){
           //使用希尔增量的方式,即每次折半
           d=d/2;
           for (int x=0;x<d;x++){
               for (int i=x+d;i<array.length;i=i+d){
                   int temp=array[i];
                   int j;
                   for (j=i-d;(j>=0)&&(array[j]>temp);j=j-d){
                       array[j+d]=array[j];
                   }
                   array[j+d]=temp;
               }
           }
       }
   }

​ 5.3优化

  • 因为每一轮希尔增量之间是等比的,这就导致了希尔增量存在盲区,于是有其他更严谨的增量方式
  • Hibbard增量:1,3,7,15...通项公式就是2(k-1),这种最坏时间复杂度是O(n3/2)
  • Sedgewick增量:1,5,19,41,109..通项公式就是(9 * 4^k-9 * 2^k + 1 ),这种最坏时间复杂度是O(n^4/3)
  • 希尔排序是不稳定的
    6堆排序

​ 6.1概念

  • 把无序数组构建成二叉堆,需要从小到大排序,则构建最大堆;需要从大到小排序,则构建最小堆
  • 循环删除堆顶元素,替换成二叉堆的末尾,调整堆产生新的堆顶
  • 时间复杂度是O(nlogn),空间复杂度是O(1),是不稳定的

​ 6.2代码实现

 //下沉调整
    //array:要调整的堆 parentIndex:要下沉的父节点 length:堆的有效大小
    public static void downAdjust(int[] array,int parentIndex,int length){
        //temp保存父节点的值,用于最后的赋值
        int temp=array[parentIndex];
        int childIndex=2*parentIndex+1;
        while (childIndex<length){
            //如果有右孩子,且右孩子大于左孩子的值,则定位到右孩子
            if (childIndex+1<length && array[childIndex+1]>array[childIndex]){
                childIndex++;
            }
            //如果父节点大于任何一个孩子的值,则直接跳出
            if (temp>=array[childIndex]){
                break;
            }
            array[parentIndex]=array[childIndex];
            parentIndex=childIndex;
            childIndex=2*childIndex+1;
        }
        array[parentIndex]=temp;
    }

    //堆排序(升序)
    public static void heapSort(int[] array){
        //1把无序数组构建成最大堆
        for (int i=array.length/2;i>=0;i--){
            downAdjust(array,i,array.length-1);
        }
        System.out.println(Arrays.toString(array));
        //2循环删除堆顶元素,移到集合尾部,调整堆产生新的堆顶
        for (int i=array.length-1;i>0;i--){
            int temp=array[i];
            array[i]=array[0];
            array[0]=temp;
            //下沉,调整最大堆
            downAdjust(array,0,i);
        }
    }


    public static void main(String[] args) {
        int[] array = {1, 3, 2, 6, 5, 7, 8, 9, 10, 0};
        heapSort(array);
        System.out.println(Arrays.toString(array));
    }

7归并排序

​ 7.1概念

  • 把大组不断对半分,分成小组,直到只剩两个,然后两两对比排序,最后归并成大组
  • 归并操作需要三步
  • 第一步:创建一个额外的大集合,用于存储归并结果,长度是两个小集合之和
  • 第二步:从左到右逐一比较两个小集合中的元素,把较小的元素优先放入大集合
  • 第三步:从另一个还有剩余元素的集合中,把剩余元素按顺序复制到大集合尾部
  • 归并排序时间复杂度是O(nlogn),空间复杂度是O(n),是稳定的

​ 7.2代码

public static void mergeSort(int[] array, int start, int end){
    if(start<end){
        //折半拆成两个小集合,分别进行扫描
        int mid=(start+end)/2;
        mergeSort(array,start,mid);
        mergeSort(array,mid+1,end);
        //把两个有序小集合,归并成一个大集合
        merge(array,start,mid,end);
    }
}
private static void merge(int[] array,int start,int mid,int end){
        //开辟额外大集合,设置指针
        int[] tempArray=new int[end-start+1];
        int p1=start;
        int p2=mid+1;
        int p=0;
        //比较两个小集合的元素,依次放入大集合
        while ((p1<=mid) && (p2<=end)){
            if (array[p1]<=array[p2]){
                tempArray[p++]=array[p1++];
            }
            else {
                tempArray[p++]=array[p2++];
            }
        }
        //左侧小集合还有剩余,依次放入大集合尾部
        while (p1<=mid){
            tempArray[p++]=array[p1++];
        }
        //右侧小集合还有剩余,依次放入大集合尾部
        while (p2<=end){
            tempArray[p++]=array[p2++];
        }
    }

8计数排序

​ 8.1概念

  • 计数排序使用一个数组,该数组中每一个下标位置的值代表数列中对应整数出现的次数
  • 适用于一定范围内的整数排序,在取值范围不是很大的情况下,它的性能甚至快过那些时间复杂度为O(nlogn)的排序
  • 如果原始数列的规模是n,最大和最小整数的差值是m,则时间复杂度是O(n+m),空间复杂度是O(m)

​ 8.2代码

public static int[] countSort(int[] array){
        //1得到数列的最大值和最小值
        int max=array[0];
        int min=array[0];
        for (int i=1;i<array.length;i++){
            if (array[i]>max){
                max=array[i];
            }
            if (array[i]<min){
                min=array[i];
            }
        }
        //2根据数列的最大值和最小值确定统计数组的长度
        int[] countArray=new int[max-min+1];
        //3遍历数组,填充统计数组
        for (int i=0;i<array.length;i++){
            countArray[array[i]]++;
        }
        //4遍历统计数组,输出结果
        int index=0;
        int[] sortedArray=new int[array.length];
        for (int i=0;i<countArray.length;i++){
            for (int j=0;j<countArray[i];j++){
                sortedArray[index++]=i;
            }
        }
        return sortedArray;
    }
  • 当数列最大和最小值差距过大时,并不适用计数排序
  • 当数列元素不是整数时,也不适用计数排序

9桶排序

​ 9.1概念

  • 如一串数据:0.5,0.84,2.18,3.25,4.5
  • 创建若干个桶,每个桶区有一定区间跨度
  • 具体需要建立多少个桶,如何确定桶的区间范围,有很多种方式,这里创建桶的数量等于原始数列的元素数量。即5个,区间跨度=(最大值-最小值)/(桶的数量-1)
  • 每个桶内部再使用归并排序
  • 总体时间复杂度为O(n),空间复杂度为O(n)
  • 桶排序性能并非绝对稳定,如果元素分布不均匀,极端情况下,第一个桶有n-1个元素,最后一个桶有1个元素,时间复杂度就退化为O(nlogn)

​ 9.2代码

 public static double[] buckSort(double[] array){
        //1得到数列的最大值和最小值,并算出差值d
        double max=array[0];
        double min=array[0];
        for (int i=1;i<array.length;i++){
            if (array[i]>max){
                max=array[i];
            }
            if (array[i]<min){
                min=array[i];
            }
        }
        double d=max-min;
        
        //2初始化桶
        //所有桶都保存在ArrayList集合中,每一个桶都被定义为一个链表LinkedList<Double> 这样便于在尾部插入元素
        int bucketNum=array.length;
        ArrayList<LinkedList<Double>> buckList = new ArrayList<>(bucketNum);
        for (int i=0;i<bucketNum;i++){
            buckList.add(new LinkedList<Double>());
        }
        
        //3遍历元素数组,将每个元素放入桶中
        for (int i=0;i<array.length;i++){
            int num=(int) ((array[i]-min)*(bucketNum-1)/d);
            buckList.get(num).add(array[i]);
        }
        
        //4对每个桶内部进行排序
        for (int i=0;i<buckList.size();i++){
            //JDK底层采用了归并排序或归并排序的优化版
            Collections.sort(buckList.get(i));
        }
        
        //5输出全部元素
        double[] sortedArray = new double[array.length];
        int index=0;
        for (LinkedList<Double> list:buckList){
            for (double element:list){
                sortedArray[index]=element;
                index++;
            }
        }
        return sortedArray;
    }

10基数排序

​ 10.1概念

  • 为了有效处理诸如手机号,英文单词等复杂元素的排序,仅仅靠一次技术排序很难实现,这时需要基数排序
  • 基数排序把排序工作拆分成多个阶段,每一个阶段只根据一个字符进行计数排序,一共排序k轮(k是字符串的长度)
  • 基数排序既可以从高位优先进行排序(MSD),也可以从低位优先进行排序(LSD)
  • 如果排序的字符串长度不规则,就以最长字符串为准,其他不足的字符串在末尾补0就行了
  • 如果原始数列的规模是n,最大和最小整数的差值是m,k是字符串的最大长度(也就是执行k次计数排序),时间复杂度是O(k(n+m)),空间复杂度是O(n+m)
    11大总结
排序算法 时间复杂度 空间复杂度 稳定性
冒泡排序 O(n^2) O(1) 稳定
鸡尾酒排序 O(n^2) O(1) 稳定
选择排序 O(n^2) O(1) 不稳定
插入排序 O(n^2) O(1) 稳定
希尔排序 O(n^4/3)... O(1) 不稳定
快速排序 O(nlogn) O(logn) 不稳定
堆排序 O(nlogn) O(1) 不稳定
归并排序 O(nlogn) O(n) 稳定
计数排序 O(n+m) O(m) 稳定
桶排序 O(n) O(n) 稳定
基数排序 O(k(n+m)) O(n+m) 稳定
posted @ 2021-10-16 18:39  fao99  阅读(50)  评论(0)    收藏  举报