01 | 简单而又复杂的排序算法

简单而复杂的排序算法

目前市面上常见的排序算法主要有两种:

  • \(O(n^2)\):选择排序,冒泡排序,插入排序、希尔排序(有时能\(nlogn\)
  • \(O(nlogn)\):快速排序、归并排序、堆排序、
  • \(O(n+k)\):计数排序、桶排序
  • \(O(n*k)\):基数排序

算法优化往往与排序思想一点而通,本文主要介绍十大排序算法,并对其中一些经典排序算法优化

\(O(n^2)\)算法

选择、冒泡、插入是时间复杂度为\(O(n^2)\)的算法

选择排序——排序之源

原始选择排序

核心思想:在一组数据中,每次选择一个最小值放到数组最前边

特点:

  • 简单
  • 效率低下
public void selectSort(Integer[] arr) {
        int n = arr.length;
        for (int i = 0; i < n; i++) {
            /*初始化minIndex指向i*/
            int minIndex = i;
            /*循环不变量->[i+1,n)找到最小值的索引,i最终指向最小值*/
            for (int j = i + 1; j < n; j++) {
                if (arr[minIndex] > arr[j]) {
                    minIndex = j;
                }
            }
            Community.swap(arr, i, minIndex);
        }
    }

优化

优化核心思想:每一轮找到当前未处理元素的最大值和最小值

public void selectSortOptimization(Integer[] arr) {
        /*
        l,r指向arr的左右两边(最大、最小值该填充的位置)
        min,max分别指向最未检查元素的最大值索引和最小值索引
        */
        int l = 0;
        int r = arr.length - 1;
        while (l <= r) {
            int minIndex = l;
            int maxIndex = r;
            /*每一轮查询保证arr[maxIndex]>=arr[minIndex]*/
            if (arr[maxIndex] < arr[minIndex]) {
                Community.swap(arr, maxIndex, minIndex);
            }
            /*循环不变量:
            l,r的位置不变:指向最小、最大值应该插入的位置
            maxIndex、minIndex含义不必拿:找到最小最大值对应的索引*/
            for (int i = l + 1; i < r; i++) {
                if (arr[i] > arr[maxIndex]) {
                    maxIndex = i;
                }
                if (arr[i] < arr[minIndex]) {
                    minIndex = i;
                }
            }
            /*交换位置*/
            if (minIndex != l) {
                Community.swap(arr, minIndex, l);
            }
            if (maxIndex != l) {
                Community.swap(arr, maxIndex, r);
            }
            /*前进*/
            l++;
            r--;
        }

每次找到未检索区域的最大值和最小值,并与指针指向的数组位置交换

冒泡排序——分角角逐

核心思想:顺序性的两两比较,相互角逐,每轮角逐出最大的值,就像是冒泡一样

public void bubbleSort(Integer[] arr) {
        int n = arr.length - 1;
        /*n-1轮即可,最后一次不需要冒泡*/
        for (int i = 0; i < n - 1; i++) {
            for (int j = 1; j < n - i; j++) {
                if (arr[j] < arr[j - 1]) {
                    Community.swap(arr, j, j - 1);
                }
            }
        }
    }

冒泡排序优化

  • 采用标志位提前中止冒泡排序
  • 每次仅考虑未排好序的部分
public void bubbleSortOptimization(Integer[] arr) {
        int n = arr.length - 1;
        while (true) {
            /*newN记录最近一次应该冒泡的位置*/
            int newN = 0;
            boolean isSwap = false;
            for (int i = 1; i <= n; i++) {
                if (arr[i] < arr[i - 1]) {
                    Community.swap(arr, i, i - 1);
                    isSwap = true;
                    newN = i;
                }
            }
            /*newN之后不考虑冒泡了*/
            n = newN;
            /*没有冒泡就中止循环*/
            if (!isSwap) {
                break;
            }
        }
    }

插入排序——我是赌王

核心思想:插入排序就像是赌王整理扑克牌一样,每次把一个数放到应该插入的地方

public void insertSort(Integer[] arr) {
        int n = arr.length;
        for (int i = 1; i < n; i++) {
            /*j从i-1开始,找i的位置*/
            for (int j = i; j > 0; j--) {
                if (arr[j - 1] > arr[j]) {
                    Community.swap(arr, j, j - 1);
                }else {
                    /*之前的都是有序的,一直找到i应该插入的位置,就没有插入排序的必要了*/
                    break;
                }
            }
        }
    }

实现方式:

  • 循环不变量:约定j不断靠近i应该插入的位置(一步一步的)
  • 找到即可退出此次循环

插入排序优化

核心思想:swap这个操作太费时啦,可以存储一份当前值arr[j],然后arr[j-1]之前的值全部往后覆盖,直到找到该插入的位置

public void insertSortAlgorithm(Integer[] arr) {
        int n = arr.length;
        for (int i = 1; i < n; i++) {
            /*备份arr[i]*/
            int temp = arr[i];
            for (int j = i; j > 0; j--) {
                if (temp < arr[j - 1]) {
                    arr[j] = arr[j - 1];
                } else {
                    /*j依然是temp该插入的位置*/
                    arr[j] = temp;
                    break;
                }
            }
        }
    }

希尔排序——我是更牛逼的插入排序

核心思想:多路插入排序,将插入排序抽样为多个子序列,然后对各个子序列插入排序,使得数据越来越趋近有序化。

public void shellSort(Integer[] arr) {
        int n = arr.length;
        /*抽样率,每次抽样的间隔*/
        int gap = n / 2;
        while (gap > 0) {
            /*找到每个小组的最后一个目标,对其插入排序*/
            for (int i = gap; i < n; i++) {
                /*插入排序开始了*/
                int j = i;
                int temp = arr[i];
                while (j - gap >= 0 && temp < arr[j - gap]) {
                    arr[j] = arr[j - gap];
                    j -= gap;
                }
                arr[j] = temp;
            }
            gap /= 2;
        }
    }

\(O(n^2)\)算法特性说明

算法 优点 缺点
选择排序 简单 低效率,无论怎样都需要两次遍历
冒泡排序 比选择排序效率高 比插入排序慢
插入排序 对近乎有序的数组排序性能好,甚至比\(O(nlogn)\)的算法块 无序的效率不如\(O(nlogn)\)

\(O(nlogn)\)算法

快速排序——最伟大的发明

  • 核心思想:递归,先大范围排序,逐渐缩小包围圈
  • 重要基石:partition,最最最重要的实现
public void quickSort(Integer[] arr) {
        int n = arr.length;
        /*[0,n-1]快速排序*/
        quickSort(arr, 0, n - 1);
    }

    /**
     * @param arr arr
     * @param l   左边界
     * @param r   右边界
     */
    private void quickSort(Integer[] arr, int l, int r) {
        /*递归中止条件*/
        if (l >= r) {
            return;
        }

        int p = partition(arr, l, r);
        quickSort(arr, l, p - 1);
        quickSort(arr, p + 1, r);
    }

    /**
     * @param arr arr[l,r]
     * @param l   left
     * @param r   right
     * @return p
     */
    private int partition(Integer[] arr, int l, int r) {
        int p = arr[l];
        int j = l;
        /*
         * arr[l+1,j]<p,arr[j+1,i-1]>=p
         * j指向小于P的最后一个数*/
        for (int i = l + 1; i <= r; i++) {
            if (arr[i] < p) {
                /*找到比P小的数,J挪动一位,然后交换位置*/
                j++;
                Community.swap(arr, i, j);
            }
        }
        Community.swap(arr, l, j);
        return j;
    }

优化一:使用插入排序优化快排

  • 近乎有序(数组较小)的情况下,插入排序比快排效率高
  • \(r-l <= 15\),选用插入排序
private void quickSortOptimizationOne(Integer[] arr, int l, int r) {
        if (r - l <= 15) {
            new InsertSort().insertSortOptimization(arr);
            return;
        }
        int p = partition(arr, l, r);
        quickSort(arr, l, p - 1);
        quickSort(arr, p + 1, r);

    }

优化二:随机快排

  • 对于大的近乎有序的数组,原始快排性能非常差,因为它选择第一个位置的数字(近乎最小的数字),非常不均衡(快排,快得就是把数组分成两个近似相同量级的数组排序,有点类似于二分查找的感觉

  • 改进核心思想:p值随机选择,不选取最左边的元素

private int partitionOptimizationOne(Integer[] arr, int l, int r) {
        /*随机选取后边一个数,与l交换*/
        Community.swap(arr, l, (int) (Math.random() * (r - l + 1)) + l);
        int p = arr[l];
        int j = l;
        /*
         * arr[l+1,j]<p,arr[j+1,i-1]>=p
         * j指向小于P的最后一个数*/
        for (int i = l + 1; i <= r; i++) {
            if (arr[i] < p) {
                /*找到比P小的数,J挪动一位,然后交换位置*/
                j++;
                Community.swap(arr, i, j);
            }
        }
        Community.swap(arr, l, j);
        return j;
    }

优化三:双路快排

  • 上述排序的缺点:对于数字重叠度高的数组,排序效率低(原因:重叠度高的数字会使拆分的数组分布不均)
  • 解决方法双路快排或者三路快排

image-20200306091515149

核心思想(@波波老师的思路):

image-20200306091742215

private int partitionOptimizationTwo(Integer[] arr, int l, int r) {
        /*随机选取后边一个数,与l交换*/
        Community.swap(arr, l, (int) (Math.random() * (r - l + 1)) + l);
        int p = arr[l];
        int i = l + 1;
        int j = r;
        /*
         * 循环不变量:[l+1,i-1]<=P [j+1,r]>=P
         * */
        while (true) {
            /* arr[i]<p 防止连续出现的数字归在一方*/
            while (i <= r && arr[i] < p) {
                i++;
            }
            while ((j >= 0 && arr[j] > p)) {
                j--;
            }
            /*检查一下是否过界*/
            if(i>j){
                break;
            }
            /*找到第一个不符合条件的两个位置*/
            Community.swap(arr,i,j);
            j--;
            i++;

        }
        /*最后交换目标到期应该到的位置*/
        Community.swap(arr,l,i);
        return i;
    }

优化四:三路快排

  • 核心思想:设置lt、gt指针,分别指向小于目标和大于目标的最后一个值,而[lt,i-1]指向等于目标的值
public void quickSort3Ways(Integer[] arr, int l, int r) {
        if(r-l<=15){
            new InsertSort().insertSortOptimization(arr);
            return;
        }
        /*随机选取后边一个数,与l交换*/
        Community.swap(arr, l, (int) (Math.random() * (r - l + 1)) + l);
        int p = arr[l];
        /*lt [l+1,lt]<p
         * gt [gt,r]>p*/
        int lt = l;
        int gt = r + 1;
        /*
         * 循环不变量:[lt+1,i-1]=p
         * */
        int i = l + 1;
        while (i < gt) {
            if (arr[i] > p) {
              //此时不需要更新i的值由于,gt的值还没检测
                gt--;
                Community.swap(arr, i, gt);
            } else if (arr[i] < p) {
                lt++;
                Community.swap(arr, lt, i);
              //此时需要更新i的值,由于前边运算已经检测这个值等于p,所以不需要再检测一次i
                i++;
            } else {
                i++;
            }
        }
        Community.swap(arr, l, lt);

        quickSort3Ways(arr, l, lt - 1);
        quickSort3Ways(arr, gt, r);

归并排序——铁杵成针

核心思想:

  • 先解决小问题,一点一点解决大问题
  • 每次都是二分——这个舒服
public void mergeSort(Integer[] arr) {
        int n = arr.length;
        mergeSort(arr, 0, n - 1);
    }

    private void mergeSort(Integer[] arr, int l, int r) {
        if (r - l <= 15) {
            new InsertSort().insertSortOptimization(arr);
            return;
        }
        /*找到中间点mid*/
        int mid = l + (r - l) / 2;
        mergeSort(arr, l, mid);
        mergeSort(arr, mid + 1, r);
        merge(arr, l, mid, r);
    }

    /**
     * @param arr arr
     * @param l   l
     * @param mid mid
     * @param r   r
     *            arr[l,mid] arr[mid+1,r]
     */
    private void merge(Integer[] arr, int l, int mid, int r) {
        Integer[] front = Arrays.copyOfRange(arr, l, mid);
        Integer[] tail = Arrays.copyOfRange(arr, mid + 1, r);
        /*front*/
        int i = 0;
        /*tail*/
        int j = 0;
        /*arr*/
        int k = l;
        while (i < front.length && j < tail.length) {
            if (front[i] >= tail[j]) {
                arr[k] = tail[j];
                j++;
            } else if (front[i] < tail[j]) {
                arr[k] = front[i];
                i++;
            }
            k++;
        }
        /*没弄完继续弄*/
        while (j < tail.length) {
            arr[k] = tail[j];
            k++;
            j++;
        }
        while (i < front.length) {
            arr[k] = front[i];
            k++;
            i++;
        }
    }

优化

  • 对近乎有序的数组效果不好
  • 原因:递归到两个子部分后,不管他们是否有序都会进行merge操纵
  • 改进思路:可以判断一下mid和mid+1,若他们俩是有序的不需要merge了,hiahiahia👀
private void mergeSort(Integer[] arr, int l, int r) {
        if (r - l <= 15) {
            new InsertSort().insertSortOptimization(arr);
            return;
        }
        /*找到中间点mid*/
        int mid = l + (r - l) / 2;
        mergeSort(arr, l, mid);
        mergeSort(arr, mid + 1, r);
        if (arr[mid] > arr[mid + 1]) {
            merge(arr, l, mid, r);
        }
    }

自底向上的归并排序

  • 不用递归
  • 不需要索引
  • 就是这么牛逼
/*定义最小的归并尺寸SZ*/
        for (int sz = 1; sz < n; sz += sz) {
            /*i表示每一轮归并的起始位置*/
            for (int i = 0; i < n-sz; i += sz + sz) {
                /*i<n-sz:确保i+sz存在才merge,否则不需要merge*/
                /*[i,i+sz-1] [i+sz,i+sz+sz-1]*/
                if(arr[i+sz-1]>arr[i+sz]){
                    /*需要merge*/
                    mergeBu(arr,i,i+sz-1,Math.min(i+sz+sz-1,n-1));
                }
            }

        }

堆排序——一个不太熟但很牛逼的排序

堆结构特性如下:

  • 层级分明的大小关系
  • 可以用数组表示

image-20200307104027246

上代码:

public void heapSort(Integer[] arr) {
        int n = arr.length;
        heapify(arr);
        int j = n - 1;
        while (j >= 1) {
            /*拿出最上方的值(最大值)
             * 与最后的值交换位置(理应放到最后)*/
            Community.swap(arr, j, 0);
            /*边界[0,j-1],j个元素
             * 整理成堆*/
            siftDown(arr, 0, j);
            /*走下一个j*/
            j--;
        }
    }

    private void heapify(Integer[] arr) {
        int n = arr.length;
        for (int i = parent(n - 1); i >= 0; i--) {
            /*从有子节点的最后一个节点开始,
            * 由下往上siftdown*/
            siftDown(arr, i, n);
        }
    }

    private int parent(int i) {
        return (i - 1) / 2;
    }

    private void siftDown(Integer[] arr, int i, int n) {
        /*有左孩子时会考虑siftdown操作*/
        while (leftChild(i) < n) {
            int j = leftChild(i);
            /*找到最大的子节点*/
            if (j + 1 < n && arr[j] < arr[j + 1]) {
                j += 1;
            }
            if (arr[j] < arr[i]) {
                break;
            }
            /*下沉*/
            Community.swap(arr, j, i);
            /*更新i,检查下一次下沉情况*/
            i = j;
        }
    }

    private int leftChild(int i) {
        return 2 * i + 1;
    }

二叉堆

索引堆

posted @ 2020-03-07 11:07  mhp  阅读(485)  评论(0)    收藏  举报