(六)希尔/快速/归并/堆排序

一 希尔排序

1.1 插入排序的问题

我们看简单的插入排序可能存在的问题. 数组 arr = {2,3,4,5,6,1} 这时需要插入的数 1(最小)
结论: 当需要插入的数是较小的数时,后移的次数明显增多,对效率有影响.

1.2 基本介绍

希尔排序是希尔(Donald Shell)于 1959 年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序

1.3 思想

希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含 的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止

image.png

image.png

1.4 代码实现

        public static void shellSort3(int[] arr) {
        
        int temp = 0;
        int count = 0;
        //缩小增量,对每一组进行简单插入排序
        for (int gap = arr.length / 2; gap > 0; gap /= 2) {
            //为什么从gap开始依次增量排序。因为前面的0-gap-1是各组的第一个元素(有序表)。
            for (int i = gap; i < arr.length; i++) {
                insertI(arr,i,gap);
            }
            System.out.println("希尔排序第" + (++count) + "轮 =" + Arrays.toString(arr));
        }
    }

    
    /**
     * 插入arr[i]到所在组正确位置的代码(insertI)
     * 已经保证i所在组前面的元素已经是有序的
     * @param arr
     * @param i
     * @param gap
     */
    private static void insertI(int[] arr, int i, int gap) {
        //下一个比较的下标
        int compareIndex = i - gap;
        int temp  = arr[i];//注意刚开始就要保留该值
        while(compareIndex >= 0 && arr[compareIndex] > temp) {
            //后移
            arr[compareIndex+gap] = arr[compareIndex];
            compareIndex -= gap;
            
            
        }
        /*
         * 此时要么没找到比他更小的,compareIndex=-1。放本组第一个=compareIndex+gap
         * 要么找到了比他更小的,本次插入位置应该在compareIndex的后面。compareIndex+gap
         */
        
        arr[compareIndex+gap] = temp;
        
    }

1.5 小结

  1. 增量=分组数。增量越大,分组越多。增量最小时(为1),分组数量也是1.
  2. 每个分组内部进行简单插入排序

二 快速排序

2.1 基本介绍

快速排序(Quicksort)是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分割成独立的两 部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列

原理:
快速排序,说白了就是给基准数据找其正确索引位置的过程.

image.png

image.png

2.2 个人理解

  1. 为什么首先从后半部分开始?能不能从前半部分开始?
    我觉得是要看基准值的下标。假如我们用第一个元素作为基准值,我们用temp存储了它的值。然后从后半部分开始的话,当high的下标所在值小于基准值的话。就会用low下标的位置来存储之前发现的小值.
    之后开始反向遍历low,发现大的,又用high的位置来存储。

这样的话我们会发现一个规律

  1. 当一个下标的值被存储时(temp或其他下标),那这个下标就需要存储其他下标(不排除原下标)的值。比如:刚开始low下标的值被temp存储了,那之后high发现的小值就被low下标所在位置存储。

2.3 代码实现

package com.nrsc.sort;

public class QuickSort {
    public static void main(String[] args) {
        int[] arr = { 49, 38, 65, 97, 23, 22, 76, 1, 5, 8, 2, 0, -1, 22 };
        quickSort(arr, 0, arr.length - 1);
        System.out.println("排序后:");
        for (int i : arr) {
            System.out.println(i);
        }
    }

    private static void quickSort(int[] arr, int low, int high) {

        if (low < high) {
            // 找寻基准数据的正确索引
            int index = getIndex(arr, low, high);

            // 进行迭代对index之前和之后的数组进行相同的操作使整个数组变成有序
            quickSort(arr, 0, index - 1);
            quickSort(arr, index + 1, high);
        }

    }

    private static int getIndex(int[] arr, int low, int high) {
        // 基准数据
        int tmp = arr[low];
        while (low < high) {
            // 当队尾的元素大于等于基准数据时,向前挪动high指针
            while (low < high && arr[high] >= tmp) {
                high--;
            }
            // 如果队尾元素小于tmp了,需要将其赋值给low
            arr[low] = arr[high];
            // 当队首元素小于等于tmp时,向前挪动low指针
            while (low < high && arr[low] <= tmp) {
                low++;
            }
            // 当队首元素大于tmp时,需要将其赋值给high
            arr[high] = arr[low];

        }
        // 跳出循环时low和high相等,此时的low或high就是tmp的正确索引位置
        // 由原理部分可以很清楚的知道low位置的值并不是tmp,所以需要将tmp赋值给arr[low]
        arr[low] = tmp;
        return low; // 返回tmp的正确位置
    }
}


1.4 总结

  1. 快速排序基准值两边的并不是有序的,只是都是比基准值小或大。

三 归并排序

3.1 基本介绍

(Merge)是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。 将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。归并排序算法稳定,数组需要O(n)的额外空间,链表需要O(log(n))的额外空间,时间复杂度为O(nlog(n)),算法不是自适应的,不需要对数据的随机读取

3.2 基本思想

image.png

3.3 理解

咋一看,总感觉分治的思想和快速排序很象。都是讲整体分,然后都有递归
区别

  1. 快速是按基准值分,两边的元素可能不一致。
  2. 归并在分的阶段没有排序,在治的阶段排序(治完局部有序,保证两个局部融合的时候元素时有序的
  3. 快速不需要治的阶段

3.4 代码实现

1、申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
2、设定两个指针变量,最初位置分别指向两个已经排好序的数组序列的起始位置
3、比较两个指针所指向的元素,选择相对小的元素放入到申请的合并空间里,并移动此指针到下一位置;
4、继续重复步骤3直到某一指针达到序列尾;
5、当一个指针到达一个序列尾时,将另一序列剩下的所有元素直接复制到合并序列尾。

package com.atguigu;

public class MergeSort {
    //两路归并算法,两个排好序的子序列合并为一个子序列
    public void merge(int []a,int left,int mid,int right){
        int []tmp=new int[a.length];//辅助数组
        int p1=left,p2=mid+1,k=left;//p1、p2是检测指针,k是存放指针

        while(p1<=mid && p2<=right){
            if(a[p1]<=a[p2])
                tmp[k++]=a[p1++];
            else
                tmp[k++]=a[p2++];
        }

        while(p1<=mid) tmp[k++]=a[p1++];//如果第一个序列未检测完,直接将后面所有元素加到合并的序列中
        while(p2<=right) tmp[k++]=a[p2++];//同上

        //复制回原素组
        for (int i = left; i <=right; i++) 
            a[i]=tmp[i];
    }

    /**
     * 递归保证了总是分无可分阶段,才会治
     * @param a
     * @param start
     * @param end
     */
    public void mergeSort(int [] a,int start,int end){
        if(start<end){//当子序列中只有一个元素时结束递归
            /*
             * 分
             */
            int mid=(start+end)/2;//划分子序列
            mergeSort(a, start, mid);//对左侧子序列进行递归排序
            mergeSort(a, mid+1, end);//对右侧子序列进行递归排序
            
            /*
             * 治
             */
            merge(a, start, mid, end);//合并
        }
    }

    public void test(){
        int[] a = { 49, 38, 65, 97, 76, 13, 27, 50 };
        mergeSort(a, 0, a.length-1);
        System.out.println("排好序的数组:");
        for (int e : a)
            System.out.print(e+" ");
    }
}

四 堆排序

参考:
https://blog.csdn.net/g1607058603/article/details/80796097

4.1 小结

  1. 什么是堆?
    一种数据结构与,分为大顶堆(每个非叶子节点都比其孩子节点的值大)和小顶堆。
    堆是一种特殊的完全二叉树。

  2. 主要步骤有哪些?
    先要构建大顶堆(小顶堆),然后依次去掉堆顶,并把最后一个元素放在堆顶。结束标志,堆中没元素了

  3. 如何表示堆?
    用数组表示。根据观察得出:左节点下标=父节点下标*2+1

  4. 构建时需要从堆中的最后一个元素开始调整吗?
    不要,需要从最后一个非叶子节(下标=arr.length()/2-1)点开始遍历。
    初始化构建微观上是自上而下的(即每次交换后都需要继续往下检查,交换后的节点是否比子节点都大),宏观上是自下而上(即从最后一个非叶子节点开始)

  5. 为什么之后只需要自上而下调整?
    这个之前也比较纠结!
    先分析:

  • 从第二次开始调整,我们可以发现堆已经都是有序的(大顶堆或小顶堆)。当我们把最后一个元素放在堆顶上后,堆可能会无效。所以我们需要调整。从上到下调整是因为:只要调整后的值大于子节点,我们就不需要考虑以这个子节点为堆顶的堆是否有序(必然有序)。所以结束调整的信号也是:调整后的值大于子节点
  1. 思想思路
    堆顶元素可能不在一个正确的位置。我们就需要不断调整它的位置,需要先拿到这个元素的值(用temp存储),如果该元素的现在位置(下标一直在变)的两个子节点都比他小。那就停止调整了。

4.2 代码实现

    public static int[] heapSort(int[] target) {

        //判断数组,即判断堆是否存在
        if (target != null && target.length > 1) {
            // 从初始堆的最后一个非叶子节点开始调整
            int pos = target.length / 2 - 1;// 因为索引从0开始,第N/2-1处(N为节点数量)的节点正好是二叉树的最后一个非叶子节点
            while (pos >= 0) {

                //从初始堆的最后一个非叶子节点开始,从右到左,从下到上的调整各个元素
                /*
                 * pos一直在减小,即堆一直在在变大
                 * 这里需要知道
                 */
                shiftDown(target, pos, target.length - 1);
                pos--;
            }

            // 经过一轮堆调整后,将大根堆的根元素与堆的最后一个元素交换
            for (int i = target.length - 1; i > 0; i--) {
                int temp = target[i];// 将最大堆的第一个元素和最后一个元素交换
                target[i] = target[0];
                target[0] = temp;
                shiftDown(target, 0, i - 1);// 此时只要将第一个元素放到合适的位置就行,因为其他位置都已经是有序的了
            }
            return target;
        }
        return target;
    }

    /**     
     * @description 自上而下调整为最大堆,将需要调整的元素放在合适的位置。
    以start元素为堆顶,开始调整
     * @author rico       
     * @created 2017年5月25日 上午9:45:40     
     * @param target
     * @param start
     * @param end     
     */ 
    private static void shiftDown(int[] target, int start, int end) {
    
        //将节点i换到适合自己的位置,一直和子节点比较,
        //一直向下走,直到不能走了为止
        int i = start;//i是父节点
        int j = 2 * start + 1;//节点i左孩子的索引
        int temp = target[i];//从该元素开始调整堆
        while (j <= end) {// 迭代条件,否则就不是叶子节点
            //否则就是单叶子
            if (j < end && target[j + 1] > target[j]) {//找出较大子女,将节点i的左孩子和右孩子进行比较
                j = j + 1; //右孩子比较大,存储较大的叶子节点下标
            }
            if (target[j] <= temp) {// 父亲大于子女,这个temp是不变的,是要放置在正确位置的那个元素
                break;//不操作,结束调整。
            } else {//父亲小于子女
                target[i] = target[j];//将子女中较大的值赋给父亲
                i = j;//将较大的子节点作为根节点,比较该根节点和其子女和值大小,看是否需要调整。
                j = 2 * j + 1;//继续调整。以j为堆顶
            }
        }
        //结束之后,将最开始的堆顶元素放到合适位置的下标。不排除i=start
        target[i] =temp;
        
    }

参考

  1. https://blog.csdn.net/nrsc272420199/article/details/82587933
  2. https://www.cnblogs.com/lizhonghai0209/p/5080002.html
  3. https://blog.csdn.net/qq_36442947/article/details/81612870
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容

  • 概述 排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部...
    蚁前阅读 5,102评论 0 52
  • 1.插入排序—直接插入排序(Straight Insertion Sort) 基本思想: 将一个记录插入到已排序好...
    依依玖玥阅读 1,179评论 0 2
  • 排序的相关概念 排序的分类 根据在排序过程中带排序的记录是否全部被放置在内存中,排序分为:内排序外排序 1.内排序...
    NotFunGuy阅读 5,080评论 6 219
  • 概述 排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部...
    printf200阅读 732评论 0 0
  • 概述 排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部...
    闲云清烟阅读 741评论 0 6