经典排序算法分析

1.算法概述


1.1 算法分类

算法分类

1.2 算法复杂度

算法复杂度

1.3 相关概念
  • 稳定性:如果a原本在b前面,而a=b,排序之后a仍然在b的前面,叫做稳定,如果a原本在b前面,而a=b,排序之后a可能会出现在b的后面,叫做不稳定
  • 稳定性的重要性:
    • 在实际的应用中,我们交换的不一定只是一个整数,而可能是一个很大的对象,交换元素存在一定的开销;
    • 在基数排序中,即,我们要对多个关键词多次排序,这个时候,就一定要使用稳定算法

2. 冒泡排序


冒泡排序是最慢的排序算法。在实际运用中它是效率最低的算法。它通过一趟又一趟地比较数组中的每一个元素,使较大的数据下沉,较小的数据上升。它是O(n^2)的算法。

2.1 算法描述
  • 比较相邻的元素。如果第一个比第二个大,就交换它们两个;

  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;

  • 针对所有的元素重复以上的步骤,除了最后一个;

  • 重复步骤1~3,直到排序完成。

2.2 动图演示

冒泡排序

2.3 代码实现
 /*
    * 冒泡排序
    * */
    public void bubbleSort(int[] nums) {
        for (int i = 0; i < nums.length; i++) {
            for (int j = 0; j < nums.length - 1 - i; j++) {
                if (nums[i]>nums[j]){//比较元素
                    //交换元素
                    int tmp=nums[i];
                    nums[i]=nums[j];
                    nums[j]=tmp;
                }
            }
        }
    }

3.快速排序


3.1 算法描述

快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

  • 从数列中挑出一个元素,称为 “基准”(pivot)一般以最左边的值为 “基准” ;
  • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
3.2 动图演示

快速排序

3.3 代码实现
 /*
    * 快速排序
    * */
    public void quickSort(int[] arr, int low, int high) {
        if (low < high) {
            int i = split(arr, low, high);    //划分数组并获得比较元素位置
            quickSort(arr, low, i - 1);    //对比较元素左边进行排序
            quickSort(arr, i + 1, high);    //对比较元素右边进行排序
        }
    }

    private int split(int a[], int low, int high) {
        int i = low;    //i指向比较元素的期望位置
        int x = a[i];    //将该数组第一个元素设置为比较元素
        //从数组的第二个元素起开始遍历,若找到的元素大于比较元素,则跳过
        for (int j = low + 1; j <= high; j++)
            //若找到了小于比较元素的数,则将其与前面较大的数进行交换
            if (a[j] <= x) {
                i++;
                swap(a, a[i], a[j]);
            }
        swap(a, a[low], a[i]);    //将比较元素交换到期望位置
        return i;
    }

    private void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

4.直接插入排序


4.1 算法描述

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

  • 从第一个元素开始,该元素可以认为已经被排序;
  • 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  • 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  • 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  • 将新元素插入到该位置后;
  • 重复步骤2~5。
4.2 动图演示

直接插入排序

4.3 代码实现
 /*
    * 直接插入排序
    * */
    public void insertionSort(int[] nums){
        for (int i = 1; i < nums.length; i++) {
            int tmp = nums[i];
            for (int j = i; j >0&&tmp<nums[j-1]; j--) {//从后向前扫描,进行比较
                    nums[j] = nums[j - 1];
                    nums[j - 1] = tmp;
            }
        }
    }

5.希尔排序


Shell排序通过将数据分成不同的组,先对每一组进行排序,然后再对所有的元素进行一次插入排序,以减少数据交换和移动的次数。其中分组的合理性会对算法产生重要的影响,现在多用D.E.Knuth的分组方法。

5.1 算法描述

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:

  • 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
  • 按增量序列个数k,对序列进行k 趟排序;
  • 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
5.2 动图演示

希尔排序

5.3 代码实现
/*
    * 希尔排序
    * */
    public void shellSort(int[] nums){
    	//增量gap,并逐步缩小增量
        for (int gap = nums.length/2; gap >0 ; gap/=2) {
        //从第gap个元素,逐个对其所在组进行直接插入排序操作
            for (int i = gap; i < nums.length; i++) {
                int tmp = nums[i];
                for (int j = i; j >=gap && tmp<nums[j-gap] ; j-=gap) {
                    nums[j]=nums[j-gap];
                    nums[j-gap]=tmp;
                }
            }
        }
    }

6.选择排序


6.1 算法描述

n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

  • 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
  • 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
  • 重复第二步,直到所有元素均排序完毕。
6.2 动图演示

选择排序

6.3 代码实现
/*
* 选择排序
* */
public void selectionSort(int[] nums){
    for (int i = 0; i < nums.length; i++) {
        int min = i;//最小元素的下标
        for (int j = i+1; j < nums.length; j++) {
            if(nums[j]<nums[min])//交换最小元素的下标
                min=j;
        }
        int tmp = nums[i];
        nums[i] = nums[min];
        nums[min] = tmp;
    }
}

7.堆排序


堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:

img

7.1 算法描述
  • 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
  • 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
  • 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
7.2 动图演示

堆排序

7.3 代码实现
/*
    * 堆排序
    * */
    public void heapSort(int[] arr){
        //1.构建大顶堆
        for(int i=arr.length/2-1;i>=0;i--){
            //从第一个非叶子结点从下至上,从右至左调整结构
            adjustHeap(arr,i,arr.length);
        }
        //2.调整堆结构+交换堆顶元素与末尾元素
        for(int j=arr.length-1;j>0;j--){
            int tmp = arr[0];
            arr[0]=arr[j];
            arr[j]=tmp;//将堆顶元素与末尾元素进行交换
            adjustHeap(arr,0,j);//重新对堆进行调整
        }

    }
    public static void adjustHeap(int []arr,int i,int length){
        int left = 2 * i + 1;
        int right = 2 * i + 2;
        int largest = i;
        //左儿子大于父节点
        if (left < length && arr[left] > arr[largest]) {
            largest = left;
        }
        //右儿子大于父节点
        if (right < length && arr[right] > arr[largest]) {
            largest = right;
        }
        if (largest != i) {
            int tmp = arr[i];
            arr[i]=arr[largest];
            arr[largest]=tmp;
            adjustHeap(arr, largest, length);
        }
    }

8.归并排序


归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

8.1 算法描述
  • 把长度为n的输入序列分成两个长度为n/2的子序列;
  • 对这两个子序列分别采用归并排序;
  • 将两个排序好的子序列合并成一个最终的排序序列。
8.2 动图演示

归并排序

8.3 代码实现
public static void sort(int []arr){
        int []temp = new int[arr.length];//在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
        sort(arr,0,arr.length-1,temp);
    }
    private static void sort(int[] arr,int left,int right,int []temp){
        if(left<right){
            int mid = (left+right)/2;
            sort(arr,left,mid,temp);//左边归并排序,使得左子序列有序
            sort(arr,mid+1,right,temp);//右边归并排序,使得右子序列有序
            merge(arr,left,mid,right,temp);//将两个有序子数组合并操作
        }
    }
    private static void merge(int[] arr,int left,int mid,int right,int[] temp){
        int i = left;//左序列指针
        int j = mid+1;//右序列指针
        int t = 0;//临时数组指针
        while (i<=mid && j<=right){
            if(arr[i]<=arr[j]){
                temp[t++] = arr[i++];
            }else {
                temp[t++] = arr[j++];
            }
        }
        while(i<=mid){//将左边剩余元素填充进temp中
            temp[t++] = arr[i++];
        }
        while(j<=right){//将右序列剩余元素填充进temp中
            temp[t++] = arr[j++];
        }
        t = 0;
        //将temp中的元素全部拷贝到原数组中
        while(left <= right){
            arr[left++] = temp[t++];
        }
    }

9.计数排序


计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

9.1 算法描述
  • 找出待排序的数组中最大和最小的元素;
  • 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
  • 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
  • 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
9.2 动图演示

计数排序

9.3 代码实现
public int[] countingSort(int[] A,int maxValue) {
        int[] B = new int[A.length];
        int[] C = new int[maxValue+1];

        for(int i=0;i<=A.length-1;i++) {
            C[A[i]]++;//表示数组A[i]的元素个数
        }
        for(int i=1;i<maxValue;i++) {
            C[i] = C[i]+C[i-1];
        }
        for(int i = A.length-1;i>=0;i--) {
            B[C[A[i]]-1] = A[i];
            C[A[i]] = C[A[i]] - 1;
        }
        return B;
    }

10.基数排序


基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。

10.1 算法描述
  • 取得数组中的最大数,并取得位数;
  • arr为原始数组,从最低位开始取每个位组成radix数组;
  • 对radix进行计数排序(利用计数排序适用于小范围数的特点);
10.2 动图演示

基数排序

10.3 代码实现
public void radixSort(int[] arr) {
        //待排序列最大值
        int max = arr[0];
        int exp;//指数
        //计算最大值
        for (int anArr : arr) {
            if (anArr > max) {
                max = anArr;
            }
        }
        //从个位开始,对数组进行排序
        for (exp = 1; max / exp > 0; exp *= 10) {
            //存储待排元素的临时数组
            int[] temp = new int[arr.length];
            //分桶个数
            int[] buckets = new int[10];
            //将数据出现的次数存储在buckets中
            for (int value : arr) {
                //(value / exp) % 10 :value的最底位(个位)
                buckets[(value / exp) % 10]++;
            }
            //更改buckets[i],
            for (int i = 1; i < 10; i++) {
                buckets[i] += buckets[i - 1];
            }
            //将数据存储到临时数组temp中
            for (int i = arr.length - 1; i >= 0; i--) {
                temp[buckets[(arr[i] / exp) % 10] - 1] = arr[i];
                buckets[(arr[i] / exp) % 10]--;
            }
            //将有序元素temp赋给arr
            System.arraycopy(temp, 0, arr, 0, arr.length);
        }
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值