数据结构系列之排序算法(泡沫排序、选择排序、插入排序、希尔排序)

数据结构之Sort Algorithm

最近开始学习数据结构的内容,在udemy上学完了一门Java数据结构和算法的课程。因为之前也没有接触过Java,所以就只当作数据结构的原理课来上,接下来所有的介绍也是着重介绍原理。

写这篇文章的主要目的是督促自己梳理总结学过的知识,促进理解,同时也希望能帮助到需要了解相关知识的同学。

首先是数据结构算法这两个基本定义的介绍。

数据结构,可以简单理解为数据被组织储存起来的方式。本系列会介绍的数据结构分别有 ArrayStackQueueHashtableTreeHeap以及Set

现在无论是在统计学或者是机器学习领域,大家都十分热衷于讨论算法。那么算法究竟是什么呢?通俗一点讲,算法就是完成一个特定任务的说明书,说明书里面会按照合理的顺序, 详尽地描述完成任务所需的步骤。

接下来介绍第一个基本的数据结构:Array。相信只要接触过编程语言的同学, (例如Python,Java等), 对Array都不会感到陌生。Array有四个区别于其他数据结构的特质,分别是:(1)Array中的所有元素在Memory中都是以连续的形式储存的。假如将Memory比喻成一个书柜,而Array是一套系列丛书,你要将这套系列丛书放到书柜上,肯定是将他们放在一个位置上,不可能硬生生拆开分开放的。(2)Array中的每一个元素在Memory中都占据一样的空间。也就是说,即使Array中的某一个元素看起来似乎比另一个元素需要更多的储存空间,他们占据的储存空间也必须是相同的。(3)Array中的元素在储存空间中的位置非常好确定。只要你知道某个Array的第一个元素在Memory中的位置,那么你就可以很轻易地推断出其他任何一个元素在Memory中的位置,因为Array的每个元素都是以连续的形式储存在Memory中。(4)假设知道目标元素在Array中的具体位置(即Index)在Array中提取元素的time complexity是O(1),即常数级的time complexity; 假设不知道目标元素的具体位置,那么提取此元素的time complexity为O(N), 即线性级的time complexity。

为了避免部分同学事先不知道time complexity这个概念,我在这简单介绍一下什么是time complexity(下文翻译成 耗时)。耗时,指的并不是真正意义上的用时长短,而是指实现某算法需要完成的步骤的多少,或是需要占据的储存空间的大小。现在储存空间的大小限制已经不是一个什么问题了,因为现在你可以用很低的成本去获取非常大的储存空间。所以,现在说的耗时,更多的是指实现某算法需要完成的步骤的多少。假设现在有一个泡茶算法,步骤分别如下:(1)取茶包(2)将茶包放入杯中 (3)将100ml热水倒入杯中 (4)当杯子七分满时,停止倒水(杯子容量是1000ml)。这个时候,当你实现这个泡茶算法时,你需要按顺序完成步骤(1)、(2)、(3),并重复步骤(3)直至满足算法终止条件为止,即杯子七分满时。也就是说你需要完成(2+17)个步骤。假设这个杯子跟一个房子一样大,那么你需要完成(2 + 1n)个步骤,n会取一个非常大的值,2可以忽略不计,那么这就是一个线性耗时,通常记为O(N)。通常还有常数级耗时O©,对数级耗时O(logN),平方级耗时O(N2)等。这些耗时按效率由高往低排分别是:O© > O(N) > O(logN) > O(N2)。耗时这个概念非常重要,往往会成为衡量算法优劣的重要指标。

讲完Array的定义和基本特征,接下来就可以进入以Array为载体的排序算法的学习了。通过了解各种排序算法,我们可以对Array作为数据结构的特性有更加深入的理解。

【Bubble Sort】
Bubble Sort,泡沫排序, 是一个通过比较相邻两个元素大小并交换位置的方式来排序的算法。讲排序算法,用过于笼统的描述过于苍白无力了,所以我也不企图用什么精确的话语来概括它的原理了,让我们直接进入实操部分。

假设我们现在有Array = [80,51,12,33,64], 我们想将其按从小到大的顺序排序。元素的上标是其index。

令loop = 1, i = 0, j = 1, loop表示排序的轮数(max(loop) = 4),i与j表示要进行对比的相邻两个元素的index。
(1) 我们先将8与5进行比较,8 > 5, 所以8与5交换位置, 得到: [50,81,12,33,64]; i = 0 + 1 = 1, j = 1 + 1 = 2;
(2) 继续将8与1进行比较, 8 > 1, 所以8与1交换位置,得到:[50,11,82,33,64]; i = 1 + 1 = 2, j = 2 + 1 = 3;
(3) 8与3比较,8 > 3, 8与3交换位置,得到:[50,11,32,83,64]; i = 2 + 1 = 3, j = 3 + 1 = 4
(4) 8与6比较,8 > 6, 8与6交换位置,得到:[50,11,32,63,84]; j = 4 + 1 = 5 超出index的范围([0,4]),第一轮排序结束。

第二轮排序开始,loop = 1 + 1 = 2, 再令i = 0, j = 1。
(1) 将5与1进行比较,5 > 1, 5与1交换位置,得到:[10,51,32,63,84];i = 0 + 1 = 1, j = 1 + 1 = 2;
(2) 将5与3进行比较,5 > 3, 5与3交换位置,得到:
[10,31,52,63,84];i = 1 + 1 =2, j = 2 + 1 =3;
(3) 将5与6进行比较,5 < 6, 5位置不变,得到:
[10,31,52,63,84];i = 2 + 1 = 3, j = 3 + 1 = 4;
(4) 将6与8进行比较,6 < 8, 6位置不变,得到:
[10,31,52,63,84];i = 3 + 1 = 4, j = 4 + 1 =5 超出index的范围([0,4]),第二轮排序结束。

第三轮排序开始, loop = 2 + 1 = 3, 再令i = 0, j = 1。
重复以上步骤,得到:[10,31,52,63,84];

第四轮排序开始,loop = 3 + 1 = 4, 再令i = 0, j = 1。
重复排序步骤,得到:[10,31,52,63,84];此时loop达到了其最大值,泡沫排序结束。最终的排序结果是:[10,31,52,63,84];

【Selection Sort】
Selection Sort,即选择排序, 是一个将Array分为无序和有序两部分,通过逐渐扩大有序部分,缩小无序部分来实现排序过程的算法。笼统的话不多说,我也说不好,还是直接进入实战吧。

假设我们现在有Array = [80,51,12,33,64, 135], 我们想将其按从小到大的顺序排序。元素的上标是其index。

(1) 首先,我们需要找出目标Array中的最小值,作为第一个有序部分。Array中的最小值为1,将1与8交换位置,得到:
[{10}有序,{51,82,33,64, 135}无序];可以看到,目标Array现在被分为两个部分,一个是有序部分{10}, 一个是无序部分{51,82,33,64, 135};接下来我们需要开始逐渐扩大有序部分的范围,缩小无序部分的范围,实现排序的目的。
(2) 从无序部分中找出最小值3, 将其与无序部分的第一个元素5交换位置,此时有序部分由{10}扩展为{10, 31}, 无序部分缩小为{82,53,64, 135},得到:[{10, 31}有序,{82,53,64, 135}无序];
(3) 继续从无序部分找出最小值5, 将其与无序部分第一个元素8交换位置,此时有序部分由{10, 31}扩展为{10, 31, 52}, 无序部分缩小为{83,64, 135}, 得到:[{10, 31, 52}有序,{83,64, 135}无序];
(4) 继续从无序部分找出最小值6, 将其与无序部分第一个元素8交换位置,此时有序部分由{10, 31, 52}扩展为{10, 31, 52, 63}, 无序部分缩小为{84, 135}, 得到:[{10, 31, 52, 63}有序,{84, 135}无序];
(5) 继续从无序部分中找出最小值8, 8已经位于正确的位置,因此有序部分由{10, 31, 52, 63}扩展为{10, 31, 52, 63, 84}, 无序部分缩小为{135}, 得到:[{10, 31, 52, 63, 84}有序, {135}无序];
(6) Array中的(NArray长度 - 1)个元素已属于有序部分,Array已完成排序,选择排序结束。

【Insertion Sort】
Insertion Sort,即插入排序,与Selection Sort相似,它也将Array分为有序部分以及无序部分,并通过逐渐扩大有序部分,缩小无序部分的方法来实现对Array的排序,但插入排序采取了跟Selection Sort不同的确定第一个有序部分以及逐步扩大有序部分缩小无序部分的方法。话不多说,开始实践吧!

假设我们现在有Array = [80,51,12,33,64, 135], 我们想将其按从小到大的顺序排序。元素的上标是其index。

(1) 首先,我们需要确定第一个有序部分。Insertion Sort直接将原始Array的第一个元素确定为有序部分,因此我们有:[{80}有序,{51,12,33,64, 135}无序];
(2) 接着, 我们需要将无序部分中的元素逐步插入到有序部分中。无序部分中第一个目标插入元素是5,我们将5与有序部分中的所有元素按照从右到左的顺序进行比较,寻找插入位置。目前有序部分只有一个元素8, 5 < 8, 因此将5插入8的前方,得到:[{50, 81}有序,{12,33,64, 135}无序];
(3) 继续将目前无序部分的第一个元素1插入有序部分。将1分别与有序部分的元素按从右到左的顺序一一比较,1 < 8,继续向左比较,1 < 5, 有序部分元素被穷尽,因此将1插入5的前方,得到:[{10, 51, 82}有序,{33,64, 135}无序];
(4) 继续将目前无序部分的第一个元素3插入有序部分。将3分别与有序部分的元素按从右到左的顺序一一比较,3 < 8,继续向左比较,3 < 5, 继续向左比较,3 > 1,因此将3插入5的前方,1的后方,得到:[{10, 31, 52, 83}有序,{64, 135}无序];
(5) 继续将目前无序部分的第一个元素6插入有序部分。将6分别与有序部分的元素按从右到左的顺序一一比较,6 < 8,继续向左比较,6 > 5,因此将6插入8的前方,5的后方,得到:[{10, 31, 52, 63, 84}有序,{135}无序];
(6) 此时Array中的(NArray长度 - 1)个元素已经位于有序部分中, Array已完成排序,插入排序结束。

【Shell Sort】
Shell Sort, 即希尔排序,可以理解为插入排序的2.0版本, 它在插入排序的基础上引进了Gap Value的概念,不再将目标元素与相邻(Gap = 1)的元素做比较,而是将目标元素与距其有更大距离差(即Gap > 1)的元素做比较,并随着排序的进行逐步减少Gap到Gap = 1。Gap的引入大大减少了排序的步骤(相较于Insertion Sort而言)。让我们来看一下Gap Value是怎么在希尔排序中发挥作用的吧!

假设我们现在有Array = [80,51,12,33,64, 135], 我们想将其按从小到大的顺序排序。元素的上标是其index。

(1) 首先我们需要确定Gap Value的初始值。Gap Value的确定有非常多种方法(即 Gap Sequence),选择不同的方法会显著影响排序的耗时。感兴趣的同学可以在https://en.wikipedia.org/wiki/Shellsort#Gap_sequences查询不同的Gap Sequence。一般我们采用Knuth Sequence, Gap = (3k - 1)/2。我们要根据Array的大小谨慎选择k值,保证Gap value小于Array 的长度的三分之一。为了简化说明,本例子采用了更加简单的方法,即令Gap = NArray的长度 / 2 = 6 / 2= 3
(2) 确定完Gap Value,就可以正式进入到排序环节了。令 i = j = Gap = 3 ,将index = j =3(33)与index = j - Gap = 0(80)的元素进行比较,3 < 8, 交换位置,得到:[30,51,12,83,64, 135];
(4) i = j = 3 + 1 = 4, 将index = j(64) 和 index = j - Gap = 1(51)的元素进行比较,6 > 5, 位置保持不变,得到:[30,51,12,83,64, 135];
(5) i = j = 4 + 1 = 5,将index = j (135)和 index = j - Gap = 2(12)的元素相比,1 < 13, 位置保持不变,得到:[30,51,12,83,64, 135];
(6) i = j = 5 + 1 = 6 > NArray最大index, 第一轮排序结束;
(7) 在前面已经提到,希尔排序会在排序过程中逐渐减少Gap值,在本例子中,采用 Gap = Gap / 2的方式减少Gap值,因此在第二轮排序中用到的Gap = 3 / 2 = 1。当Gap = 1时,采用插入排序法。在此不重复演示插入排序的方法。

接下来会陆续继续更新Merge SortQuick SortCount SortRadix Sort等常见的排序算法,敬请期待(抱拳)。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值