[数据结构] 排序

目录

[1. 排序的概念](#1. 排序的概念)

[2. 常见的排序算法](#2. 常见的排序算法)

[3. 常见排序算法的实现](#3. 常见排序算法的实现)

[3.1 插入排序](#3.1 插入排序)

[3.1.1 基本思想](#3.1.1 基本思想)

[3.1.2 直接插入排序](#3.1.2 直接插入排序)

[3.1.3 希尔排序](#3.1.3 希尔排序)

[3.2 选择排序](#3.2 选择排序)

[3.1.1 基本思想](#3.1.1 基本思想)

[3.1.2 直接选择排序](#3.1.2 直接选择排序)

[3.1.3 堆排序](#3.1.3 堆排序)

[3.3 交换排序](#3.3 交换排序)

[3.3.1 基本思想](#3.3.1 基本思想)

[3.3.2 冒泡排序](#3.3.2 冒泡排序)

[3.3.3 快速排序](#3.3.3 快速排序)

[3.3.4 快速排序的优化](#3.3.4 快速排序的优化)

[3.3.5 快速排序的非递归方法实现](#3.3.5 快速排序的非递归方法实现)

[3.5 归并排序](#3.5 归并排序)

[3.5.1 递归实现](#3.5.1 递归实现)

[3.5.2 非递归实现](#3.5.2 非递归实现)

[3.5.3 归并排序的应用场景](#3.5.3 归并排序的应用场景)

[4. 排序算法的总结](#4. 排序算法的总结)

[5. 其他非比较排序](#5. 其他非比较排序)

[5.1 计数排序](#5.1 计数排序)

[5.2 基数排序](#5.2 基数排序)

[5.3 桶排序](#5.3 桶排序)


1. 排序的概念

排序:排序就是指将将一连串的数据,按照某种规则排列成从小到大或者从大到小的顺序。

稳定性:稳定性就是指在排序的过程中,如果两个数相同,排序前两个数的先后顺序与排序后两个数的先后顺序相同,则说明这种排序是稳定的。

**内部排序:**数据元素少的时候可以全部放在内存中排序。

外部排序:数据元素多时,就只能部分数据放在内存中排序,其余元素放在外存中,跟内存来回交换元素。

2. 常见的排序算法

3. 常见排序算法的实现

3.1 插入排序

3.1.1 基本思想

插入排序就是将一个数据插入到一组已经排好序的数据当中,知道所有数据都插入进来,此时数据就是有序的。比如说打扑克牌时候,整理牌的过程。

3.1.2 直接插入排序

直接插入排序是依次将元素插入,第一次插入时有序,第二次插入时,将插入元素和前面元素比较,排成有序插入,依次类推,知道插入所有元素。

思路:

这里定义一个i下标作为将要插入的元素的下标,定义一个j下标作为插入位置的前一个位置,设置一个变量tmp存储插入的元素,将j下标依次递减,将前面的元素跟插入的元素比较,插入元素小的话,就将比较的元素后移一位,依次类推。

代码实现:

java 复制代码
    /**
     * 直接插入排序
     * 时间复杂度:O(n^2)
     * 空间复杂度:O(1)
     * 稳定性:稳定
     */

    public void insertSort(int[] array) {
        for (int i = 0; i < array.length; i++) {
            int tmp = array[i];
            int j = i-1;
            for (; j >= 0; j--) {
                if(tmp < array[j]) {
                    array[j+1] = array[j];
                }else {
                    //array[j+1] = tmp;
                    break;
                }
            }
            array[j+1] = tmp;
        }
    }

3.1.3 希尔排序

希尔排序是先选定一个数n,将这组数据每间隔n的数据设置为1组,将每组数据进行排序,在将这个数n减小,再分组,直到这个数等于1,排序结束。

在希尔排序中,分组是根据n来分的,每间隔n的数据算一组。

代码实现如下:

java 复制代码
    /**
     * 希尔排序
     * 时间复杂度:O(n^1.3   - n^1.5)
     * 空间复杂度:O(1)
     * 稳定性:不稳定
     */
    public void shellSort(int[] array) {
        int n = array.length;
        while(n > 1) {
            n = n / 2;
            shell(array,n);
        }
    }
    private void shell(int[] array, int n) {
        for (int i = n; i < array.length; i++) {
            int tmp = array[i];
            int j = i - n;
            for (; j >= 0; j -= n) {
                if(tmp < array[j]) {
                    array[j+n] = array[j];
                }else {
                    break;
                }
            }
            array[j+n] = tmp;
        }
    }

3.2 选择排序

3.1.1 基本思想

选择排序是从未排序的序列中找到最小的或最大的一个元素,将该元素与未排序的第一个元素进行行交换,以此类推。

3.1.2 直接选择排序

这种排序方式也就是选择排序方式.

代码如下:

java 复制代码
    /**
     * 选择排序
     * 时间复杂度:O(n^2)
     * 空间复杂度:O(1)
     * 稳定性:不稳定
     */
    public void selectSort(int[] array) {
        for (int i = 0; i < array.length; i++) {
            int min = i;
            for (int j = i+1; j < array.length; j++) {
                if(array[j] < array[min]) {
                    min = j;
                }
            }
            swap(array,i,min);
        }
    }
    private void swap(int[] array,int i,int j) {
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

3.1.3 堆排序

堆排序先创建一个大根堆,然后再将大根堆排序成小根堆,将最顶上元素跟最后一个元素交换位置,再将前面元素转换为大根堆。

java 复制代码
   /**
     * 堆排序
     * 时间复杂度:O(n*logn)
     * 空间复杂度:O(1)
     * 稳定性:不稳定
     */
    public void heapSort(int[] array) {
        //创建一个大根堆
        //O(n)
        createLeap(array);
        //根排序
        //O(n*logn)
        int i = array.length-1;
        while(i > 0) {
            swap(array,0,i);
            siftDown(array,0,i);
            i--;
        }
    }

    private void createLeap(int[] array) {
        int parent = (array.length - 1 -1 ) /2;
        while(parent >= 0) {
            siftDown(array,parent,array.length);
            parent--;
        }
    }

    private void siftDown(int[] array, int parent, int length) {
        int child = 2 * parent + 1;
        while(child < length) {
            if(child + 1 < length && array[child] < array[child+1]) {
                child = child + 1;
            }
            if(array[child] > array[parent]) {
                swap(array,child,parent);
                parent = child;
                child = 2*parent +1;
            }else {
                break;
            }
        }
    }

3.3 交换排序

3.3.1 基本思想

交换排序就是设置两个下标,将两个下标对应的值进行比较,小的往前面移动,大的往后面移动。

3.3.2 冒泡排序

冒泡排序是设置两个变量,第一个变量是需要比较的趟数,因为有几个元素就需要比较几趟,每趟都能将最大的元素放到最后一个位置,第二个变量是对应的下标遍历这串数据,相邻之间比较,大的数交换位置到后面,将未排序的数据比较完成就行。

代码如下:

java 复制代码
    /**
     *冒泡排序
     * 时间复杂度:O(n^2)
     * 空间复杂度:O(1)
     * 稳定性:稳定
     */
    public void bubbleSort(int[] array) {
        for (int i = 0; i < array.length; i++) {
            for (int j = 0; j < array.length - 1 - i; j++) {
                if(array[j] > array[j+1]) {
                    swap(array,j,j+1);
                }
            }
        }
    }
    private void swap(int[] array, int i, int j) {
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

3.3.3 快速排序

快速排序是先找一个位置的值tmp,然后将数据中的比tmp元素小的数据,放到tmp的左边,将比tmp大的数据放到tmp的右边。知道最后遍历成功排序。

1. Hoare版

这个版本的快速排序是将数据的第一个位置的数据设置为tmp,然后设置两个下标left和right,刚好位于数据的两端,先从右边找到小于tmp的数,再从左边找到大于tmp的数,然后交换顺序,最后left和right相遇时,将该位置的数据和left初始位置的数据交换位置。

上面是一遍的过程,当第一次排序完,tmp的数据所在位置的左边都比tmp小,右边都比tmp大,然后左边和右边继续比较,最后知道数据只剩一个时候不用比较了,停止递推。

代码如下:

java 复制代码
    /**
     * 快速排序(Hoare版)
     * 时间复杂度:最坏的情况(顺序和逆序)O(n^2) 最好的情况O(nlogn)
     * 空间复杂度:最坏情况O(n)   最好情况O(logn)
     * 稳定性:不稳定
     */
    public void quickSort(int[] array) {
        quick(array,0,array.length-1);
    }
    private void quick(int[] array, int left, int right) {
        if(left >= right) {
            return;
        }

        int pivot = partition(array, left, right);
        quick(array,left,pivot-1);
        quick(array,pivot+1,right);

    }
    //该方法在每次结束后返回left和right相遇的地方。
    private int partition(int[] array, int left, int right) {
        int first = left;
        int tmp = array[left];
        while(left < right) {
            while(left < right && array[right] >= tmp) {
                right--;
            }
            while(left < right && array[left] <= tmp) {
                left++;
            }
            swap(array,left,right);
        }
        swap(array,first,left);
        return left;
    }

2. 挖坑法

该方法是将第一个元素放到tmp中,然后从右边遍历,遇到小于tmp的元素,将该元素放到左边的坑的地方,此时右边有坑,从左边遍历,遇到大于tmp的元素,将该元素放到右边的坑位置。

代码如下:

java 复制代码
    //挖坑法
    private int partition(int[] array, int left, int right) {
        int tmp = array[left];
        while(left < right) {
            while(left < right && array[right] >= tmp) {
                right--;
            }
            array[left] = array[right];
            while(left < right && array[left] <= tmp) {
                left++;
            }
            array[right] = array[left];
        }
        array[left] = tmp;
        return left;
    }

3. 前后指针法

这种方法的思路是先将开头的数据存到tmp中,设置一个指针prev为开始位置,cur为第二个位置,当碰到cur位置的数据小于tmp,cur和prev都向前移动,当cur位置的元素大于tmp时,prev停止移动,知道cur再次遇到比tmp小的元素时,prev移动一次,然后将prev和cur位置的元素交换位置。

结束条件是cur超过了数据的最后一个元素位置。

代码如下:

java 复制代码
    //前后指针法
    private int partition(int[] array, int left, int right) {
        int prev = left;
        int cur = left + 1;
        while(cur <= right) {
            if(array[cur] < array[left] && array[++prev] != array[cur]) {
                swap(array,prev,cur);
            }
            cur++;
        }
        swap(array,prev,left);
        return prev;
    }

3.3.4 快速排序的优化

我们可以发现快速排序对于从小到大的数据和从大到小的数据的排序效率毕竟底,那么我们可以通过优化来提高快速排序的效率,让它的时间复杂度接近于O(nlogn)。

方法一:三数取中法

这里是为了提高顺序的数据,开头中间和最后的三个数进行比较,得到中间的那个数的下标,将改下标位置的数与开头的数交换位置,然后利用挖坑法排序。

代码如下:

java 复制代码
    private void quick(int[] array, int left, int right) {
        if(left >= right) {
            return;
        }

        //三数取中法
        int index = findCenter(array,left,right);
        swap(array,left,index);

        int pivot = partition(array, left, right);
        quick(array,left,pivot-1);
        quick(array,pivot+1,right);

    }

   private int findCenter(int[] array, int left, int right) {
        int tmp = (left + right) / 2;
        if(array[left] < array[right]) {
            if(array[left] > array[tmp]) {
                return left;
            }else if(array[tmp] > array[right]) {
                return right;
            }else {
                return tmp;
            }
        }else {
            if(array[left] < array[tmp]) {
                return left;
            }else if(array[tmp] < array[right]) {
                return right;
            }else {
                return tmp;
            }
        }
    }

方法二:递归到小的区间时可以考虑使用插入排序。

这种优化是当数据大小<=自己设置的值时候,就使用直接插入排序。

代码如下:

java 复制代码
        //直接插入排序
        if(right - left + 1 <= 20) {
            insertSort(array,left,right);
            return;
        }

    //当数据大小小于等于20时采用插入排序
    private void insertSort(int[] array, int left, int right) {
        for (int i = left+1; i <= right; i++) {
            int tmp = array[i];
            int j = i-1;
            for (; j >= left; j--) {
                if(array[j] > tmp) {
                    array[j+1] = array[j];
                }else {
                    break;
                }
            }
            array[j+1] = tmp;
        }
    }

3.3.5 快速排序的非递归方法实现

非递归实现快速排序,需要我们设置一个栈,将挖空排序后返回的值pivot进行判断,如果pivot+ 1 < right,说明pivot的右边有至少两个数据,此时将右边的开头下标和最后下标入栈,再出栈作为参数传给挖空法快速排序,进行排序,知道栈里面没有元素,排序完成。

代码实现:

java 复制代码
    //非递归实现快速排序
    public void quickSortNonR(int[] array) {
        int left = 0;
        int right = array.length - 1;
        Stack<Integer> stack = new Stack<>();
        //调用挖空法快速排序
        int pivot = partition(array,left,right);
        if(pivot > left + 1) {
            stack.push(left);
            stack.push(pivot - 1);
        }
        if(pivot < right - 1) {
            stack.push(pivot+1);
            stack.push(right);
        }
        while(!stack.isEmpty()) {
            right = stack.pop();
            left = stack.pop();
            pivot = partition(array,left,right);
            if(pivot > left + 1) {
                stack.push(left);
                stack.push(pivot - 1);
            }
            if(pivot < right - 1) {
                stack.push(pivot+1);
                stack.push(right);
            }
        }
    }

3.5 归并排序

3.5.1 递归实现

归并排序是利用分治法来进行排序的,排序整组数据,可以先将数据平分为两半,一直分,分到最后单个元素为一组停止,然后回溯进行排序,最后合并成一整组时排序完毕。

代码如下:

java 复制代码
      /**
     * 归并排序
     * 时间复杂度:O(nlogn)
     * 空间复杂度:O(n)
     * 稳定性:稳定
     */
 public void mergeSort(int[] array) {
        merge(array,0,array.length-1);
    }
    private void merge(int[] array, int left, int right) {
        if(left >= right) {
            return;
        }
        //拆分
        int mid = (left + right) / 2;
        merge(array,left,mid);
        merge(array,mid+1,right);
        //合并
        mergeChild(array,left,mid,right);
    }
    private void mergeChild(int[] array, int left, int mid, int right) {
        int s1 = left;
        int e1 = mid;
        int s2 = mid+1;
        int e2 = right;
        //临时数组
        int[] tmp = new int[right - left + 1];
        int k = 0;
        while(s1 <= e1 && s2 <= e2) {
            if(array[s1] < array[s2]) {
                tmp[k++] = array[s1++];
            }else {
                tmp[k++] = array[s2++];
            }
        }
        while(s1 <= e1) {
            tmp[k++] = array[s1++];
        }
        while(s2 <= e2) {
            tmp[k++] = array[s2++];
        }
        //将临时数组的值放到原来数组中
        for (int i = 0; i < tmp.length; i++) {
            array[i+left] = tmp[i];
        }
    }

3.5.2 非递归实现

非递归实现归并排序是先按照一个元素为一组,接着以2的倍数元素为一组,设置坐标i为两组相邻元素的开头位置,找到left mid right 下标,将两组元素之间进行排序。以此类推,知道最后所有元素为一组时排序完成。

代码如下:

java 复制代码
    //非递归实现归并排序
    public void mergeSortNo(int[] array) {
        int gap = 1;
        while(gap <= array.length) {
            for (int i = 0; i < array.length; i = i + 2*gap) {
                int left = i;
                int mid = left + gap - 1;
                if(mid >= array.length) {
                    mid = array.length-1;
                }
                int right = mid + gap;
                if(right >= array.length) {
                    right = array.length-1;
                }
                mergeChild(array,left,mid,right);
            }
            gap = 2 * gap;
        }
    }

3.5.3 归并排序的应用场景

当要处理很大容量的数据时,对其排序,内存中不能放下这么多的数据进行排序,我们可以利用归并排序的思想,将这些数据均分为等量的小组数据放入内存中进行排序,再将这些小组数据进行合并,就可以实现排序了。

4. 排序算法的总结

下面是个算法的时间复杂度和空间复杂度以及稳定性:

5. 其他非比较排序

5.1 计数排序

计数排序适用于数据都集中在一定的范围中,如果数据中存在差值比较大的数据,会导致空间复杂度太大,所以适用的场景比较单一。

代码实现:

java 复制代码
       /**
     * 时间复杂度:O(max(n,范围))
     * 空间复杂度:O(创建数组范围)
     * 稳定性:稳定
     * @param array
     */
    //计数排序
    public void CountSort(int[] array) {
        int min = array[0];
        int max = array[0];
        for (int i = 1; i < array.length; i++) {
            if(min > array[i]) {
                min = array[i];
            }
            if(max < array[i]) {
                max = array[i];
            }
        }
        int[] tmp = new int[max-min+1];

        for (int i = 0; i < array.length; i++) {
            tmp[array[i]-min]++;
        }
        int j = 0;
        for (int i = 0; i < tmp.length; i++) {
            while(tmp[i] != 0) {
                array[j++] = i+min;
                tmp[i]--;
            }
        }
    }

5.2 基数排序

基数排序是先设置10个队列,然后将所有的数从个位数开始将数字放到对应下标的队列中,然后从0开始的队列依次取出数据,再比较十位上的数据,,循环往复,最后比较到这组数据的最高位数据时停止。

想了解更多的可以看下面的文章:

基数排序详解

5.3 桶排序

桶排序是在计数排序的基础上改善的,设置几个桶,每个桶对应一个范围,将数据按照桶的范围放入到桶中,将桶内部的数据进行排序,再将桶中的元素取出来放到数组里面。

java 复制代码
    //桶排序
    public void bucketSort(int[] array) {
        //得到数据的最大值和最小值
        int min = array[0];
        int max = array[0];
        for (int i = 1; i < array.length; i++) {
            if(min > array[i]) {
                min = array[i];
            }
            if(max < array[i]) {
                max = array[i];
            }
        }
        //计算有几个桶
        int bucketNum = (max - min) / array.length + 1;
        ArrayList<ArrayList<Integer>> arr = new ArrayList<>(bucketNum);
        for (int i = 0; i < bucketNum; i++) {
            arr.add(new ArrayList<Integer>());
        }
        //将每个元素放到桶中
        for (int i = 0; i < array.length; i++) {
            int num = (array[i] - min) / array.length;
            arr.get(num).add(array[i]);
        }
        //对每个桶进行排序
        for (int i = 0; i < arr.size(); i++) {
            Collections.sort(arr.get(i));
        }
        //将桶里面元素放到原序列中
        int index = 0;
        for (int i = 0; i < arr.size(); i++) {
            for (int j = 0; j < arr.get(i).size(); j++) {
                array[index++] = arr.get(i).get(j);
            }
        }
    }
相关推荐
睡不醒的kun4 小时前
leetcode算法刷题的第三十四天
数据结构·c++·算法·leetcode·职场和发展·贪心算法·动态规划
吃着火锅x唱着歌4 小时前
LeetCode 978.最长湍流子数组
数据结构·算法·leetcode
Whisper_long4 小时前
【数据结构】深入理解堆:概念、应用与实现
数据结构
IAtlantiscsdn4 小时前
Redis7底层数据结构解析
前端·数据结构·bootstrap
我星期八休息4 小时前
深入理解跳表(Skip List):原理、实现与应用
开发语言·数据结构·人工智能·python·算法·list
和编程干到底6 小时前
数据结构 栈和队列、树
数据结构·算法
爱编程的化学家6 小时前
代码随想录算法训练营第十一天--二叉树2 || 226.翻转二叉树 / 101.对称二叉树 / 104.二叉树的最大深度 / 111.二叉树的最小深度
数据结构·c++·算法·leetcode·二叉树·代码随想录
shan&cen8 小时前
Day04 前缀和&差分 1109. 航班预订统计 、304. 二维区域和检索 - 矩阵不可变
java·数据结构·算法
屁股割了还要学8 小时前
【数据结构入门】排序算法(4)归并排序
c语言·数据结构·学习·算法·排序算法