当前位置:主页 > 查看内容

排序算法总结

发布时间:2021-04-21 00:00| 位朋友查看

简介:选择排序 定义 一种最简单的排序算法是这样的假定我们对一个数组进行排序那么我们可以找到数组中最小的元素然后与数组的第一个经行交换如果第一个已经是最小的那就和自己交换。然后在剩下的元素中找到最小的和第二个进行交换。以此类推最终得到一个升序的数……

选择排序

定义

一种最简单的排序算法是这样的:假定我们对一个数组进行排序那么,我们可以找到数组中最小的元素,然后与数组的第一个经行交换,如果第一个已经是最小的,那就和自己交换。然后在剩下的元素中找到最小的和第二个进行交换。以此类推,最终得到一个升序的数组。这种方式就叫做选择排序。

冒泡排序的升级版本。

代码实现

@Test
    void select() {
        int nums[] = new int[]{9,8,7,6,5,4,3,2,1,0};

        //经行选择排序
        selectSort(nums);
        for (int num : nums) {
            System.out.println(num);
        }
    }

    private void selectSort(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指向这个小的
                    min = j;
                }
                //把最小的放在剩余元素的最前面
                int tmp = nums[i];
                nums[i] = nums[min];
                nums [min] = tmp;
            }
        }
    }

时间复杂度

这里只考虑最坏的情况。

对于一个长度为N的数组,第一次就需要遍历整个数组,这个时候就是N次,接下来就是N-1,N-2……直到比较完成。所以最多比较次数就是把这些加起来等于N(N-1)/ 2,与就是O(N^2)。

插入排序

定义

斗地主都玩过,摸起来的牌都会一张一张的插入到一个合适的位置,然后正副牌看起来就是井然有序。这种插入的方式就是插入排序。

在这里插入图片描述

就比如这个,3比9小,于是插入到9的左边,然后发现,3比7小,又插入到7的左边,直到左边没有比这数小的,总的来说就是一趟过后,左边的都是有序的。

代码实现

@Test
void select() {
    int nums[] = new int[]{9,8,7,6,5,4,3,2,1,0};

    //经行选择排序
    insertSort(nums);
    for (int num : nums) {
        System.out.println(num);
    }
}

private void insertSort(int[] nums){
    //外层循环
    for (int i = 1; i < nums.length; i++) {
        for (int j = i;j > 0;j--) {
            //当前比前面的小,插入
            if (nums[j] < nums[j-1]){
                //交换
                int tmp = nums[j];
                nums[j] = nums[j-1];
                nums [j-1] = tmp;
            } else {
                break;  //没有就后移再次进入循环比较
            }
        }
    }
}

时间复杂度为:N2/4,就是O(N2),比选择排序好那么一点。

希尔排序

上面的排序每次都是相邻的元素进行比较,导致每次都需要大量的移动,而希尔排序的特点就是,每次使数组变得大致上有序,然后变得有序。每次使一个组有序,总体上就是大致有序,然后缩小这个组直到最后有序。

在这里插入图片描述

比如这个,1,7一组,6,5一组,然后这每一个组排个序,整个数组就整体上变得有序很多,然后缩小这个分组,直到最后有序。

在这里插入图片描述

代码实现

@Test
void select() {
    int nums[] = new int[]{9,8,7,6,5,4,3,2,1,0};

    //经行选择排序
    insertSort(nums);
    for (int num : nums) {
        System.out.println(num);
    }
}

private void shellSort(int[] nums){
    //选择一个增量
    int n = nums.length/2;
    //增量不等于1排序,数组只能大致有序,所以会逐步缩小到1
    while (n > 0) {
        for (int i = n; i < nums.length; i++) {
            for (int j = i; j >= n; j -= n) {
                //比较这两个大小
                if (nums[j] < nums[j-n]){
                    //交换
                    int tmp = nums[j];
                    nums[j] = nums[j-n];
                    nums [j-n] = tmp;
                }
            }
        }
        //缩小增量
        n = n / 2;
    }
}

对于这种排序,怎么选择增量就是一个最复杂的问题。

归并排序

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。

作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:

  • 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
  • 自下而上的迭代;

用一张图演示归并的过程:

在这里插入图片描述

分割归并:

在这里插入图片描述

递归

既然是一个分割合并的过程,必然需要两个指针来对着两边进行归并。

public static void main(String[] args) {
    //归并排序,定义一个数组
    int arr[] = new int[]{1,5,3,8,7,4,9,2};
    //需要递归的方法
    sort(arr,0,arr.length-1);
    for (int i : arr) {
        System.out.print(i + " ");
    }
}

//方法含义,根据数组进行排序
public static void sort(int arr[], int left, int right){
    //递归结束条件,左指针大于或者等于右指针
    if (left >= right) return;
    //二分的中间值
    int mid = left + (right - left) / 2;
    //否则进行分割
    sort(arr,left, mid);
    sort(arr,mid+1,right);
    //最后才是排序
    merge(arr,left,mid, right);
}

private static void merge(int[] arr, int left, int mid, int right) {
    //先复制这个数组,这样为了保证原来的数组不被打乱
    int tmp[] = new int[right-left+1];

    int k=0;
    int l = left;
    int r = mid+1;
    //左右指针
    while (l <= mid && r <= right){
        if(arr[l] < arr[r]){
            //左边数组小于右边,把左边的放在临时数组中
            tmp[k] = arr[l];
            l++;
        } else {
            tmp[k] = arr[r];
            r++;
        }
        k++;
    }

    //然后把上面没排完的全部放在这个新数组里面
    while (l <= mid) tmp[k++] = arr[l++];
    while (r <= right) tmp[k++] = arr[r++];

    //最后把数组赋值给原来的数组
    for (int i = 0; i < tmp.length; i++) {
        arr[left+i] = tmp[i];
    }

}

其实这是有一点像树的后续遍历,先找到叶子节点(这里先分割成最小单位)。然后在做一个排序处理。

快速排序

快排的性能在所有排序算法里面是最好的,数据规模越大快速排序的性能越优。快排在极端情况下会退化成O(n^2)的算法,因此假如在提前得知处理数据可能会出现极端情况的前提下,可以选择使用较为稳定的归并排序。

一张图解释快排:
在这里插入图片描述

仔细看,发现这个过程跟归并排序貌似是一个相反的过程,归并是先分割成最小两个单位,在逐步向上。而快排是先排序成两个分割好的再递进下去。

总结规律:

  • 选择待排序(A)中的任意一个元素pivot,该元素作为基准
  • 将小于基准的元素移到左边,大于基准的元素移到右边(分区操作)
  • A被pivot分为两部分,继续对剩下的两部分做同样的处理
  • 直到所有子集元素不再需要进行上述步骤
public static void main(String[] args) {
    //归并排序,定义一个数组
    int arr[] = new int[]{1,5,3,8,7,4,9,2};
    //需要递归的方法
    sort(arr,0,arr.length-1);
    for (int i : arr) {
        System.out.print(i + " ");
    }
}

//方法含义,根据数组进行排序
public static void sort(int arr[], int left, int right){
    int index = 0;
    //递归结束条件,左指针大于或者等于右指针
    if (left < right) {
        //将第一趟排序后的地址拿到
        index = merge(arr, left, right);
        //然后根据这个下标分割这个数组
        sort(arr, left, index - 1);
        sort(arr, index + 1, right);
    }
}

private static int merge(int[] arr, int left, int right) {
    //主要的逻辑代码,双指针左右前进
    int tmp = arr[left];    //保存中轴
    while (left < right) {
        while (left < right && arr[right] >= tmp) {
            //比中轴小,则需要交换到低端,否则直接这个下标后移
            right--;
        }
        //进行交换
        arr[left] = arr[right];
        while (left < right && arr[left] <= tmp) {
            //比中轴大就交换到高端,否则就这个下标前移
            left++;
        }
        arr[right] = arr[left];
    }
    //最后更新这个中轴
    arr[left] = tmp;
    return left;
}

堆排序

堆排序是利用这种数据结构而设计的一种排序算法,堆排序是一种**选择排序,**它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。首先简单了解下堆结构。

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

在这里插入图片描述

同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射(层序遍历的一种结构)到数组中就是下面这个样子:

在这里插入图片描述

这个数组中蕴含的一个数学公式:

大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]

小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]

至此就可以开始着手了解这个堆排序。

基本思想:将待排序构造成一个大顶堆(或者小堆)结构,此时,整个序列的最大值(最小值)一定是堆顶的这个节点,或者说是这个数组的第一个元素。然后把这个值和最后的一个节点交换,这个时候最大或者最小值不久直接搞到最后了嘛,然后把除了最后一个节点的剩下节点再构造成一个堆结构,如此反复,一个有序的数组就得到了。

public static void main(String[] args) {
    int[] nums = {16,7,3,20,17,8};
    headSort(nums);
    for (int num : nums) {
        System.out.print(num + " ");
    }
}

/**
* 堆排序
*/
public static void headSort(int[] list) {
    //构造初始堆,i一定是倒数第二层的一个节点,并且这个节点的右边的节点一定没有孩子节点。
    for (int i = (list.length) / 2 - 1; i >= 0; i--) {
        headAdjust(list, list.length, i);
    }
    //排序,将最大的节点放在堆尾,然后从根节点重新调整
    for (int i = list.length - 1; i >= 1; i--) {
        int temp = list[0];
        list[0] = list[i];
        list[i] = temp;
        headAdjust(list, i, 0);
    }
}

/**
* 调整这个节点与左右孩子的关系
* @param list
* @param len
* @param i
*/
private static void headAdjust(int[] list, int len, int i) {

    //tem 是父节点,index是左孩子,k 父节点的位置
    int k = i, temp = list[i], index = 2 * k + 1;

    while (index < len) {
        //index < len 表示有左孩子
        if (index + 1 < len) {
            //找到孩子节点中较大值的下标
            if (list[index] < list[index + 1]) {
                index = index + 1;
            }
        }

        //这个较大的孩子节点比父节点大,交换。
        if (list[index] > temp) {
            list[k] = list[index];
            k = index;
            index = 2 * k + 1;
        } else {
            break;
        }
    }
    //更新这个孩子节点的值,如果上面没有发生交换,就保持父节点不动
    list[k] = temp;
}

计数排序

现在假设一种场景,高考一般考生在几百万,有这么多的数据,但是这个数据值的分布呢,0到750这个数据,也就是不论哪个人,分数一定是在这个范围。那么如何知道一个人的排名呢?这个时候就可以统计每个分数的人数,而不是非要把几百万个数据进行一个排序!

在这里插入图片描述

核心思想:统计每个整数在序列中出现的次数,进而推导出每个整数在有序序列中的索引

public static void main(String[] args) {
    //归并排序,定义一个数组
    int arr[] = new int[]{1,5,3,8,7,4,0,0,9,2,4,5,1,7,8,9,7,4,4,5,1,2,3,2,6,5,6,9,8,7};
    //需要递归的方法
    int[] sort = sort(arr);
    for (int i : sort) {
        System.out.print(i + " ");
    }
    System.out.println();
    for (int i : arr) {
        System.out.print(i+" ");
    }

}

//方法含义,根据数组进行排序
public static int[] sort(int arr[]){
    //既然是计数,我们需要一个数组来记录每个数据出现的次数
    int count[] = new int[10];
    //然后遍历这个数组,每个数据当作下标使用
    for (int i = 0; i < arr.length; i++) {
        count[arr[i]]++;
    }
    int index = 0; //这个数组的下标,i表示这个数值
    //根据这个计数器得到数据
    for (int i = 0; i < count.length; i++) {
        int tmp = count[i]; //得到这个值的次数
        while (tmp > 0){
            arr[index++] = i;
            tmp--;
        }
    }
    return count;
}

前面的排序都是比较大小人后交换位置,这种排序算得上另辟蹊径,直接统计数量然后重新赋值整个数组。但是这种排序大部分只针对这种数字。

基数排序

计数排序是一种很容易理解的算法,适用的领域也比较小。这里给出的基数排序也用到了桶这个概念,也就是统计个数。一张动图如下:假如我们现在数组的元素是:1221, 15, 20, 3681, 277, 5420, 71, 1522, 4793。

在这里插入图片描述

在上面的例子中,我们先对个位进行count排序,然后对十位进行count排序,然后是百位和千位。最后生成最终的排序结果。

public static void main(String[] args) {
    int[] arr = new int[] { 23, 6, 9, 287, 56, 1, 789, 34, 65, 653 };
    System.out.println(Arrays.toString(arr));
    radixSort(arr);
    System.out.println(Arrays.toString(arr));
}

public static void radixSort(int[] arr) {

    // 存数组中最大的数字,为了知道循环几次
    int max = Integer.MIN_VALUE;// (整数中的最小数)
    // 遍历数组,找出最大值
    for (int i = 0; i < arr.length; i++) {
        if (max < arr[i]) {
            max = arr[i];
        }
    }

    // 计算最大数是几位数,,此方法计较绝妙
    int maxLength = (max + "").length();
    // 用于临时存储数据的数组
    int[][] temp = new int[10][arr.length];
    // 用于存储桶内的元素位置
    int[] counts = new int[arr.length];

    // 第一轮个位数较易得到余数,第二轮就得先除以十再去取余,之后百位除以一百
    // 可以看出,还有一个变量随循环次数变化,为了取余

    // 循环的次数
    for (int i = 0, n = 1; i < maxLength; i++, n *= 10) {
        // 每一轮取余
        for (int j = 0; j < arr.length; j++) {
            // 计算余数
            int ys = (arr[j] / n) % 10;
            // 把数据放在指定数组中,有两个信息,放在第几个桶+数据应该放在第几位
            temp[ys][counts[ys]] = arr[j];
            // 记录数量
            counts[ys]++;
        }

        // 记录取的数字应该放到位置
        int index = 0;
        // 每一轮循环之后把数字取出来
        for (int k = 0; k < counts.length; k++) {
            // 记录数量的数组中当前余数记录不为零
            if (counts[k] != 0) {
                for (int l = 0; l < counts[k]; l++) {
                    // 取出元素
                    arr[index] = temp[k][l];
                    index++;
                }
                // 取出后把数量置为零
                counts[k] = 0;
            }
        }
    }
}

桶排序

一句话总结:划分多个范围相同的区间,每个子区间自排序,最后合并。

桶排序是计数排序的扩展版本,计数排序可以看成每个桶只存储相同元素,而桶排序每个桶存储一定范围的元素,通过映射函数,将待排序数组中的元素映射到各个对应的桶中,对每个桶中的元素进行排序,最后将非空桶中的元素逐个放入原序列中。

在这里插入图片描述

public static void bucketSort(int[] arr){
    
    // 计算最大值与最小值
    int max = Integer.MIN_VALUE;
    int min = Integer.MAX_VALUE;
    for(int i = 0; i < arr.length; i++){
        max = Math.max(max, arr[i]);
        min = Math.min(min, arr[i]);
    }
    
    // 计算桶的数量
    int bucketNum = (max - min) / arr.length + 1;
    ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum);
    for(int i = 0; i < bucketNum; i++){
        bucketArr.add(new ArrayList<Integer>());
    }
    
    // 将每个元素放入桶
    for(int i = 0; i < arr.length; i++){
        int num = (arr[i] - min) / (arr.length);
        bucketArr.get(num).add(arr[i]);
    }
    
    // 对每个桶进行排序
    for(int i = 0; i < bucketArr.size(); i++){
        Collections.sort(bucketArr.get(i));
    }
    
    // 将桶中的元素赋值到原序列
	int index = 0;
	for(int i = 0; i < bucketArr.size(); i++){
		for(int j = 0; j < bucketArr.get(i).size(); j++){
			arr[index++] = bucketArr.get(i).get(j);
		}
	}  
}
;原文链接:https://blog.csdn.net/weixin_55086330/article/details/115409165
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!
上一篇:idea常用快捷键!(开发必备) 下一篇:没有了

推荐图文


随机推荐