从减治法到插入排序再到希尔排序

减治法和分治法

在算法学习的路上,我们必定会听过一个名词:分治法。这个算法设计思想的应用的广泛就和他的名声一样广为人知。但是不少初学者往往却会弄混减治法分治法的区别。这里引用一下 wiki 上对这个区别的介绍 Divide and conquer algorithm :

Therefore, some authors consider that the name "divide and conquer" should be used only when each problem may generate two or more subproblems. The name decrease and conquer has been proposed instead for the single-subproblem class.

可以理解为,分治法是将问题分解为多个子问题,解决这些子问题并得到最终大问题的结果;而减治法是将大问题的规模缩小为小问题,解决一个小问题而得出大问题的解。

插入排序

减治技术利用了一个问题给定实例的解和同样问题较小实例的解之间的某种关系。一旦建立起这种关系,我们就可以从顶至下,也可以从底至上地来运用这种关系。
从顶至下会自然而然地导出递归算法,典型例子是在一棵树中查找一个元素,这个不是今天文章的重点,不多赘述。
相反,从底至上就容易导出迭代的算法,从求解问题的一个较小的实例开始,所以该方法有时也被称为增量法

插入排序就是减治法的一种典型的例子:我们考虑对一个数组 A[0....n-1] 进行排序,假设我们对数组 A 的子数组 A[0....n-2] 的排序已经完成,那么只要将剩下的元素 n-1 在 A[0....n-2] 中找到合适的位置插入即可完成排序。然后我们再假设对 A[0....n-3] 的排序已经完成,那么将 n-2 插入到合适的位置即可完成 A[0....n-2] 的排序

很明显,插入排序的思想是基于递归思想的,但是我们常见的插入排序的实现代码是基于增量法的,要谈为什么使用增量法而不使用易于理解的递归算法,那就要扯到递归算法的缺点了,以后有空会补上一篇《论迭代和递归的优缺点》来细细评判这个问题了,在此就不叙述了。至于如何实现增量法版本的插入排序算法,只要我们把思想反转过来,先考虑得到只含一个元素的有序数组,然后在这个数组中找到正确的位置插入第二个元素、第三个元素……直到插入完第 n 个元素,就可以得到有序的数组 A[0....n-1] 了。

下面是插入排序算法的 Java 版实现代码:

public void insertSort(int[] arr){
        for (int i = 1; i < arr.length; i++) {
            int val = arr[i];
            int j = i - 1;
            while (j >= 0 && arr[j] > val){
                arr[j + 1] = arr[j];
                j--;
            }
            arr[j + 1] = val;
        }
    }

从算法的实现代码可以看出插入排序的基本操作是比较 arr[j] > val。所以它的性能也跟这个键值对的比较次数息息相关。

分析可见该算法的键值对比较次数取决于输入的数组。最坏情况下,每轮循环的比较次数都会走满,此时输入的数组是一个纯降序数组。比较次数为 (n - 1) * n / 2。由此也可以知道插入排序算法的性能是 O(n²) 级别。最优的情况是输入的数组是一个纯升序的数组,每轮循环比较次数仅发生一次。键值对的比较次数为 n - 1。而对于平均情况,研究表明插入排序处理无序数组时,性能是降序数组,即最坏情况的一半 n² / 4

插入排序在平均情况下的性能比最差性能快一半,以及它在处理接近有序的数组时表现出优异的性能,使它在基本排序算法中占据无可撼动的地位。

希尔排序

希尔排序的名字来自于它的提出者 DL.Shell ,它是对插入排序的改进算法。希尔排序的算法思想是将整个待排序列
(R1, R2, R3, ……, Rn)
按增量 d 划分成 d 个子序列,其中第 i (1 <= i <=d) 个子序列为
(Ri, Ri+d, Ri+2d, ……, Ri+kd)
并分别对每个子序列进行插入排序(如果以增量 d 划分原数组,原数组就有 d 个子序列,就要进行 d 次插入排序);不断减小增量 d,重复这一过程,直到 d 减少到 1,对整个数组进行一次插入排序。基于增量不断减少的特点,希尔排序也称为缩小增量排序

希尔排序前几趟排序(除了最后一次)和上面我们写的插入排序算法代码的的最大区别是这几次插入排序操作数组时下标移动的间隔 d 不是 1,而是 d 个元素!同时由于每次移动的间隔较大,可以跨越多个元素,所以实现的调整是宏观上的调整。

当希尔排序走到最后一趟排序,即增量为 1 时,得益于前几趟排序宏观上的调整,数组已经基本上有序,此时最后一趟排序就相当于最好情况下的插入排序。将前面各趟的调整看成是最后一趟的预处理,比只做一次增量为 1 的直接插入排序操作的效率高很多。

当然,我相信只是这样书面式的解释肯定不好理解希尔希尔排序为什么调大增量就能改进插入排序算法,下面引用知乎 希尔排序为什么会那么牛那么快,能够证明吗? 问题下 冒泡 用户的回答:

为啥希尔能突破 O(n²) 的界,可以用逆序数来理解,假设我们要从小到大排序,一个数组中取两个元素如果前面比后面大,则为一个逆序,容易看出排序的本质就是消除逆序数,可以证明对于随机数组,逆序数是
O(n²) 的,而如果采用“交换相邻元素”的办法来消除逆序,每次正好只消除一个,因此必须执行 O(n²) 的交换次数,这就是为啥冒泡、插入等算法只能到平方级别的原因,反过来,基于交换元素的排序要想突破这个下界,必须执行一些比较,交换相隔比较远的元素,使得一次交换能消除一个以上的逆序,希尔、快排、堆排等等算法都是交换比较远的元素,只不过规则各不同罢了

即关键是:希尔排序使用大增量一次消除一个以上的数组元素逆序

同样给出希尔排序的实现代码

public void shellSort(int[] arr){
        // 增量初始值取数组长度的一半,并每次减半直到增量为 1
        for (int d = arr.length / 2; d >= 1; d /= 2){
            // 对于增量 d 划分出来的 d 个子序列分别进行排序
            // i 是子序列的起点
            for (int i = 0; i < d; i++){
                // 增量为 d 的选择排序
                for (int j = i + d; j < arr.length; j += d){
                    int val = arr[j];
                    int k = j - d;
                    while (k >= i && arr[k] > val){
                        arr[k + d] = arr[k];
                        k -= d;
                    }
                    arr[k + d] = val;
                }
            }
        }
    }

因为希尔排序每次交换元素跨越了多个下标,所以有可能造成相同的元素在经过排序后位置发生了变化的结果,即希尔排序是不稳定的排序算法,在使用希尔排序时需注意这种不稳定的特性是否会造成不良的影响。

另外上面这段希尔排序的代码仍然不是最优情况下的希尔排序代码,希尔排序的时间复杂度是所取增量变化情况的函数,目前尚难以分析出最好的增量减少规律。

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

推荐阅读更多精彩内容