浅谈七大排序算法(详细讲解加代码实现)

目录

1. 排序的概念及引用

1.1 排序的概念

1.2 排序运用

1.3 常见的排序算法 

2. 常见排序算法的实现 

2.1 插入排序

2.1.1 基本思想

2.1.2 直接插入排序 

2.1.3 希尔排序(缩小增量排序)

2.2 选择排序

2.2.1 基本思想

2.2.2 直接选择排序

2.2.3 堆排序

2.3 交换排序

2.3.1 冒泡排序

2.3.2 快速排序

2.3.3 快速排序优化

2.3.4 快速排序非递归实现

2.3.5 快速排序总结

2.4 归并排序 

2.4.1 基本思想

2.4.2 归并排序总结

2.4.3 海量数据的排序问题

3 总结 


1. 排序的概念及引用

1.1 排序的概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持 不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳 定的;否则称为不稳定的。

内部排序:数据元素全部放在内存中的排序。 

外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

1.2 排序运用

1.3 常见的排序算法 

2. 常见排序算法的实现 

2.1 插入排序

2.1.1 基本思想

直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。实际中我们玩扑克牌时,就用了插入排序的思想。

2.1.2 直接插入排序 

  1. 假设我们有一个数组,初始时只有第一个元素是有序的。

  2. 从第二个元素开始,依次将每个元素插入到前面已经排好序的子数组中。

  3. 插入时,从后向前比较,找到合适的位置插入该元素。

/**
     * 最好情况下:O(n)  ->  数据有序的情况下  1 2 3 4 5
     * 最坏情况下:O(n^2) -> 数据逆序的情况下  5 4 3 2 1
     * 空间复杂度:O(1)
     * 稳定性:稳定的排序
     * @param array
     */
    public static void insertSort(int[] array) {
        for (int i = 1; i < array.length; i++) {
            // 用于记录插入位置的索引
            int j = i - 1;
            // 当前要插入的元素
            int tmp = array[i];
            for (; j >= 0; j--) {
                if (array[j] > tmp) {
                    array[j + 1] = array[j];
                }else {
                    break;
                }
            }    
            // 将当前元素插入到正确的位置
            array[j + 1] = tmp;
        }
    }

直接插入排序的特性总结:

1. 元素集合越接近有序,直接插入排序算法的时间效率越高

2. 时间复杂度:O(N^2)

3. 空间复杂度:O(1),它是一种稳定的排序算法

4. 稳定性:稳定 

2.1.3 希尔排序(缩小增量排序)

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成多个组, 所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,重复上述分组和排序的工作。当增量等于1时,所有记录在统一组内排好序。

    private static void shell(int[] array, int gap) {
        for (int i = gap; i < array.length; i++) {
            int tmp = array[i];
            int j = i - gap;
            for (; j >= 0; j -= gap) {
                if (array[j] > tmp) {
                    array[j + gap] = array[j];
                }else {
                    break;
                }
            }
            array[j + gap] = tmp;
        }
    }

    /**
     * 希尔排序
     * 时间复杂度在 o(n^1.25) - o(n^1.5) 之间
     * @param array
     */
    public static void shellSort(int[] array) {
        int gap = array.length;
        while (gap > 1) {
            gap /= 2;
            shell(array, gap);
        }
    }

希尔排序的特性总结:

1. 希尔排序是对直接插入排序的优化。

2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。

3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排 序的时间复杂度都不固定: 

4. 稳定性:不稳定 

2.2 选择排序

2.2.1 基本思想

每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。

2.2.2 直接选择排序

  1. 初始化:假设数组从第一个元素开始是无序的。

  2. 在每次迭代中,从未排序的部分中找到最小(或最大)的元素的索引。

  3. 将该元素与未排序部分的第一个元素交换。

  4. 重复上述过程,直到整个数组有序。

    /**
     * 选择排序
     * @param array
     */
    public static void selectSort(int[] array) {
        // 先假设第一个元素是最小的
        for (int i = 0; i < array.length; i++) {
        // 在未排序的部分中寻找真正的最小值索引
            int minIndex = i;
            for (int j = i + 1; j < array.length; j++) {
                if (array[j] < array[minIndex]) {
                    minIndex = j;
                }
            }
            // 交换
            swap(array, i, minIndex);
        }

    }

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

    /**
     * 第二种选择排序实现方法
     * @param array
     */
    public static void selectSort2(int[] array) {
        int left = 0;
        int right = array.length-1;
        while (left < right) {
            int minIndex= left;
            int maxIndex = left;
            for (int i = left + 1; i <= right; i++) {
                if (array[i] < array[minIndex]) {
                    minIndex = i;
                }
                if (array[i] > array[maxIndex]) {
                    maxIndex = i;
                }
            }
            swap(array, left, minIndex);
            if (maxIndex == left) {
                maxIndex = minIndex;
            }
            swap(array, right, maxIndex);
            left++;
            right--;
        }
    }

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

【选择排序的特性总结】

1. 选择排序非常好理解,但是效率不是很好。实际中很少使用

2. 时间复杂度:O(N^2)    不管本身的数据 是有序还是无序  都是这个时间复杂度

3. 空间复杂度:O(1)

4. 稳定性:不稳定

2.2.3 堆排序

堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。

    /**
     * 堆排序
     * 建堆的时间复杂度为o(n)
     * 向下调整的时间复杂度是o(n*logn)
     * 总的时间复杂度就是n*logn
     * @param array
     */
    public static void heapSort(int[] array) {
        createBigHeap(array);
        int end = array.length - 1;
        while (end > 0) {
            swap(array, 0, end);
            siftDown(array, 0, end);
            end--;
        }
    }

    private static void createBigHeap(int[] array) {
        for (int i = (array.length-1-1) / 2; i >= 0; i--) {
            siftDown(array, i, array.length);
        }
    }

    private static void siftDown(int[] array, int parent, int len) {
        int child = 2 * parent + 1;
        while (child < len) {
            if (child + 1 < len && 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;
            }
        }
    }

【堆排序的特性总结】

1. 堆排序使用堆来选数,效率就高了很多。

2. 时间复杂度:O(N*logN)  不管本身的数据 是有序还是无序  都是这个时间复杂度

3. 空间复杂度:O(1)

4. 稳定性:不稳定 

2.3 交换排序

基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

2.3.1 冒泡排序

    /**
     * 冒泡排序
     * 时间复杂度:O(N^2) 对数据不敏感  有序 无序都是这个复杂度!
     * 空间负责度:O(1)
     * 稳定性:稳定的排序
     *     插入排序   冒泡排序
     *
     * 加了优化之后,时间复杂度可能会变成O(n)
     * @param array
     */
    public static void bubbleSort(int[] array) {
        for (int i = 0; i < array.length - 1; i++) {
            boolean flg = false;
            for (int j = 0; j < array.length- 1- i; j++) {
                if (array[j] > array[j + 1]) {
                    swap(array, j, j + 1);
                    flg = true;
                }
            }
            if (!flg) {
                break;
            }
        }
    }

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

【冒泡排序的特性总结】

1. 冒泡排序是一种非常容易理解的排序

2. 时间复杂度:O(N^2)   不管本身的数据 是有序还是无序  都是这个时间复杂度

3. 空间复杂度:O(1)

4. 稳定性:稳定

2.3.2 快速排序

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

    //  假设按照升序对array数组中[left, right)区间中的元素进行排序
    void QuickSort(int[] array, int left, int right)
    {
        if(right - left <= 1)
            return;
    //  按照基准值对array数组的 [left, right)区间中的元素进行划分
        int div = partion(array, left, right);
    //  划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)
    //  递归排[left, div)
        QuickSort(array, left, div);
    //  递归排[div+1, right)
        QuickSort(array, div+1, right);
    }

上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,我们在写递归框架时可想想二叉树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可。 将区间按照基准值划分为左右两半部分的常见方式有:

1. Hoare法

    private static int partition(int[] array, int left, int right) {
        int i = 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, left, i);
        return left;
    }

    private static 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);  // 右树
    }

    public static void quickSort(int[] array) {
        quick(array, 0, array.length - 1);
    }

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

2.挖坑法

    /**
     * 挖坑法
     * @param array
     * @param left
     * @param right
     * @return
     */
    private static 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;
    }
    private static 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);  // 右树
    }

    public static void quickSort(int[] array) {
        quick(array, 0, array.length - 1);
    }

3. 前后指针法

    /**
     * 前后指针法
     * @param array
     * @param left
     * @param right
     * @return
     */

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

    private static 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,cur,prev);
            }
            cur++;
        }
        swap(array,prev,left);
        return prev;
    }
    private static void quick(int[] array, int start, int end) {
        if (start >= end) {
            return;
        }

        int pivot = partition(array, start, end);

        quick(array, start, pivot -1 ); //左树
        quick(array, pivot + 1, end);  // 右树
    }

    public static void quickSort(int[] array) {
        quick(array, 0, array.length - 1);
    }

2.3.3 快速排序优化

1. 三数取中法选key

为的就是将待排序序列均匀分开, 不要总是只有左边或者是右边

下面演示的是极端情况下

   /**
     * 挖坑法
     * @param array
     * @param left
     * @param right
     * @return
     */
    private static int partition2(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;
    }

   /**
     * 三数取中法
     * @param array
     * @param left
     * @param right
     * @return 返回中间数字的下标
     */
    private static int threeNum(int[] array, int left, int right) {
        int mid = (left + right) / 2;
        if (array[left] < array[right]) {
            if (array[mid] < array[left]) {
                return left;
            }else if (array[mid] > array[right]  ) {
                return right;
            }else {
                return mid;
            }
        }else {
            if (array[mid] < array[right]) {
                return right;
            }else if (array[mid] > array[left]) {
                return left;
            }else {
                return mid;
            }
        }
    }

    private static void quick(int[] array, int start, int end) {
        if (start >= end) {
            return;
        }

        int mid = threeNum(array, start, end);
        swap(array, start, mid);
        int pivot = partition(array, start, end);

        quick(array, start, pivot -1 ); //左树
        quick(array, pivot + 1, end);  // 右树
    }

    public static void quickSort(int[] array) {
        quick(array, 0, array.length - 1);
    }

2. 递归到小的子区间时,可以考虑使用插入排序

    /**
     * 挖坑法
     * @param array
     * @param left
     * @param right
     * @return
     */
    private static int partition2(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;
    }
   
    /**
     * 三数取中法
     * @param array
     * @param left
     * @param right
     * @return 返回中间数字的下标
     */
    private static int threeNum(int[] array, int left, int right) {
        int mid = (left + right) / 2;
        if (array[left] < array[right]) {
            if (array[mid] < array[left]) {
                return left;
            }else if (array[mid] > array[right]  ) {
                return right;
            }else {
                return mid;
            }
        }else {
            if (array[mid] < array[right]) {
                return right;
            }else if (array[mid] > array[left]) {
                return left;
            }else {
                return mid;
            }
        }
    }

    public static void insertSort2(int[] array, int left, int right) {
        for (int i = left + 1; i <= right; i++) {
            int j = i - 1;
            int tmp = array[i];
            for (; j >= left; j--) {
                if (array[j] > tmp) {
                    array[j + 1] = array[j];
                }else {
                    break;
                }
            }
            array[j + 1] = tmp;
        }
    }

    private static void quick(int[] array, int start, int end) {
        if (start >= end) {
            return;
        }
        // 递归到小的子区间时, 可以考虑使用直接插入排序
        if (end - start + 1 <= 20) {
            insertSort2(array, start, end);
            return;
        }

        int mid = threeNum(array, start, end);
        swap(array, start, mid);
        int pivot = partition(array, start, end);

        quick(array, start, pivot -1 ); //左树
        quick(array, pivot + 1, end);  // 右树
    }

    public static void quickSort(int[] array) {
        quick(array, 0, array.length - 1);
    }

2.3.4 快速排序非递归实现

   public static void quickSort(int[] array) {
        Stack<Integer> stack = new Stack<>();
        int start = 0;
        int end = array.length-1;

        int pivot = partition(array,start,end);
        if(pivot > start+1) {
            stack.push(start);
            stack.push(pivot-1);
        }

        if(pivot < end-1) {
            stack.push(pivot+1);
            stack.push(end);
        }

        while (!stack.empty()) {
            end = stack.pop();
            start = stack.pop();
            pivot = partition(array, start, end);
            if (pivot > start + 1) {
                stack.push(start);
                stack.push(pivot - 1);
            }
            if (pivot < end - 1) {
                stack.push(pivot + 1);
                stack.push(end);
            }
        }
    }

同样的, 也可以加入上述优化的方法。 

    public static void quickSort(int[] array) {
        Stack<Integer> stack = new Stack<>();
        int start = 0;
        int end = array.length-1;

        if(end - start +1 <= 20) {
            //直接插入排序
            insertSort2(array,start,end);
            return;
        }

        //三数取中
        int mid = threeNum(array,start,end);
        //交换
        swap(array,mid,start);

        int pivot = partition(array,start,end);
        if(pivot > start+1) {
            stack.push(start);
            stack.push(pivot-1);
        }

        if(pivot < end-1) {
            stack.push(pivot+1);
            stack.push(end);
        }

        while (!stack.empty()) {
            end = stack.pop();
            start = stack.pop();

            if(end - start +1 <= 20) {
                //直接插入排序
                insertSort2(array,start,end);
                //return; 这个地方不能return
            }else {
                mid = threeNum(array,start,end);
                //交换
                swap(array,mid,start);
                pivot = partition(array, start, end);
                if (pivot > start + 1) {
                    stack.push(start);
                    stack.push(pivot - 1);
                }
                if (pivot < end - 1) {
                    stack.push(pivot + 1);
                    stack.push(end);
                }
            }
        }
    }

2.3.5 快速排序总结

1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序

2. 时间复杂度:O(N*logN )

3. 空间复杂度:O(logN)

4. 稳定性:不稳定

2.4 归并排序 

2.4.1 基本思想

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

归并排序递归写法 

    private static void merge(int[] array,int left,int mid,int right) {

        int s1 = left;
        int e1 = mid;
        int s2 = mid+1;
        int e2 = right;

        int[] tmpArr = new int[right-left+1];
        int k = 0;//tmpArr数组的下标

        while (s1 <= e1 && s2 <= e2) {
            if(array[s1] <= array[s2]){
                tmpArr[k++] = array[s1++];
            }else {
                tmpArr[k++] = array[s2++];
                //s2++;
                //k++;
            }
        }
        while (s1 <= e1) {
            tmpArr[k++] = array[s1++];
        }
        while (s2 <= e2) {
            tmpArr[k++] = array[s2++];
        }
        for (int i = 0; i < k; i++) {
            array[i+left] = tmpArr[i];
        }

    }

    private static void mergeSortFunc(int[] array, int left, int right) {
        // 这里不应该取 >= 号 已经调试了 == 是可以的 
        if(left == right) {
            return;
        }
        int mid = (left+right) / 2;
        mergeSortFunc(array,left,mid);
        mergeSortFunc(array,mid+1,right);
        merge(array,left,mid,right);//合并
    }

    /**
     * 归并排序递归实现
     * @param array
     */
    public static void mergeSort(int[] array) {
        mergeSortFunc(array,0,array.length-1);
    }

 归并排序非递归写法

   public static void mergeSort(int[] array) {
       int gap = 1;
       while (gap < array.length) {
           for (int i = 0; i < array.length; i = i + gap*2) {
               int left = i;
               int mid = left+gap-1;
               int right = mid+gap;
               //mid  和 right 有可能会越界
               if(mid >= array.length) {
                   //纠正
                   mid = array.length-1;
               }
               if(right >= array.length) {
                   right = array.length-1;
               }
               merge(array,left,mid,right);
           }
           gap *= 2;
       }
    }

2.4.2 归并排序总结

1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。

2. 时间复杂度:O(N*logN)

3. 空间复杂度:O(N)

4. 稳定性:稳定 

2.4.3 海量数据的排序问题

外部排序:排序过程需要在磁盘等外部存储进行的排序

前提:内存只有 1G,需要排序的数据有 100G

因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序

1. 先把文件切分成 200 份,每个 512 M

2. 分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以

3. 进行2路归并,同时对 200 份有序文件做归并过程,最终结果就有序了。

3 总结 

排序算法最好时间复杂度最坏时间复杂度平均时间复杂度空间复杂度是否稳定
插入排序O(n)O(n²)O(n²)O(1)
希尔排序O(n log n)O(n²)O(n^(3/2))O(1)
选择排序O(n²)O(n²)O(n²)O(1)
堆排序O(n log n)O(n log n)O(n log n)O(1)
快速排序O(n log n)O(n²)O(n log n)O(log n)
归并排序O(n log n)O(n log n)O(n log n)O(n)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

早点睡觉1.0

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值