插入排序与冒泡排序算法的比较

110

我正在尝试理解一些排序算法,但我很难看出冒泡排序和插入排序算法的区别。

我知道两者都是O(n2),但在我看来,冒泡排序仅将数组的最大值冒泡到每次排序的顶部,而插入排序则将最小值下沉到每次排序的底部。它们不是在做完全相同的事情,只是方向不同吗?

对于插入排序,比较/潜在交换的次数从零开始逐渐增加(即0、1、2、3、4、…、n),但对于冒泡排序,这种行为也发生在排序结束时(即n、n-1、n-2、… 0)因为冒泡排序不再需要与已排序的最后一个元素进行比较。

尽管如此,似乎有共识认为插入排序通常更好。有人能告诉我原因吗?

编辑:我主要关注算法工作方式的差异,而不是其效率或渐近复杂度。


1
这在其他地方已经有很好的文档说明了:例如请参考https://en.wikipedia.org/wiki/Sorting_algorithm。在这里复制将毫无意义,一个好的答案应该是详细的。 - Bathsheba
1
@Bathsheba,75个点赞和88k的浏览量似乎不太同意;) - parsecer
@parsecer:哈!现在我必须要重新审查这些答案了。目前得票最高的答案很有用,但其他答案就不确定了。希望通过对答案进行贬值来失去一些声望分。被接受的答案中的断言“这就是为什么插入排序比冒泡排序快”的说法并不一定正确。 - Bathsheba
1
@Bathsheba 哦不 - parsecer
14个回答

153

插入排序

经过i次迭代,前i个元素已排序。

在每一次迭代中,下一个元素会在已排序的部分中冒泡,直到到达正确的位置:

sorted  | unsorted
1 3 5 8 | 4 6 7 9 2
1 3 4 5 8 | 6 7 9 2

4被冒泡到已排序的部分

伪代码:

for i in 1 to n
    for j in i downto 2
        if array[j - 1] > array[j]
            swap(array[j - 1], array[j])
        else
            break

冒泡排序

经过i次迭代,最后i个元素是最大的,并且已经有序。

在每次迭代中,遍历未排序的部分以找到最大值。

unsorted  | biggest
3 1 5 4 2 | 6 7 8 9
1 3 4 2 | 5 6 7 8 9

5从未排序的部分冒出来了

伪代码:

for i in 1 to n
    for j in 1 to n - i
         if array[j] > array[j + 1]
             swap(array[j], array[j + 1])

请注意,如果在外部循环的迭代过程中没有进行交换,则典型实现会提前终止(因为这意味着数组已经排序)。

区别

在插入排序中,元素被冒泡到已排序部分,而在冒泡排序中,最大值被冒泡到未排序部分之外。


18
谢谢,非常清晰!我认为我需要强调的主要是插入排序中的“break”语句意味着它可以提前终止每次迭代:即当它在已排序部分中找到自己的位置时。冒泡排序要求交换继续进行,直到未排序部分中的最大元素到达已排序部分,因此永远不会提前终止。总之,这是一个很棒的总结,所以+1。 - Migwell
4
我认为这应该是最佳答案 :) - Adelin
2
为了清晰度、教学价值和每个算法的主循环不变式,给它加1分。遗憾的是,它没有明确包含复杂度比较(以n的函数表示),但我认为这比被接受的答案更好,因为从中我可以看到差异。 - Honza Zidek
我可以问一下,为什么在你的插入排序伪代码中,你要在每一步都交换你的项目?如果(a [j-1]> a [j]),那么a [j] = a [j-1],否则,如果(a [j-1] <e && a [j]> e),那么a [j] = e; break;,其中e是需要排序的项目。通过这种解决方案,您不会交换已经排序好的项目,只是复制它们。期待您的解释,因为我有点困惑。 - narancs
@Karoly,我选择了我的版本,因为它更简单。你的版本略微更快,很好你指出来了。维基百科描述了两个版本。 - tom
显示剩余2条评论

48
在冒泡排序的第i次迭代中,您有n-i-1个内部迭代(总共为(n^2)/2)。但在插入排序中,您在第i步最多有i次迭代,但平均只有i/2次,因为您可以在找到当前元素的正确位置后更早地停止内部循环。 因此,您有(从0到n的总和)/ 2,即(n^2)/ 4的总和;这就是为什么插入排序比冒泡排序更快的原因。

2
算法的解释在网上随处可见,我认为。 - sasha.sochka
4
是的,但似乎原帖作者仍未理解机制之间的差异。 - UmNyobe
18
你可以假设我理解了“基础知识”。我想要的是一个比较,这个解释非常好。所以,插入排序会导致第i个元素下沉,冒泡排序会导致它上升,但插入排序不会使它掉到最底部,而只是使它落到已排序部分的正确位置。因此,它进行更少的比较/交换。这样说对吗? - Migwell
2
什么意思?“所以你有(从0到n的总和)/2,即(n ^ 2)/4总计” 这需要一些解释,请! 0 + 1 + 2 + 3 + 4 + 5 = 15; 15/2 = 7.5; 7.5 * 4 = 30; sqrt(30) = 无意义 - John Smith
@JohnSmith 我认为答案有一点错误。从1加到n的总和是n*(n+1)/2,因为它是一个三角形数。查找三角形数以获取更多解释。所以除以2后就是n*(n+1)/2。 - CognizantApe

17

还有一个区别,我在这里没有看到:

冒泡排序 每次交换需要 3 次值赋值: 首先,你必须创建一个临时变量来保存你想要向前推进的值(第 1 步),然后你必须把另一个交换变量写入刚才保存的值所在的位置(第 2 步),最后,你必须将临时变量写入其他位置 (第 3 步)。为了将变量排序到正确的位置,你必须对每个位置执行以上步骤。

使用插入排序,将要排序的变量放入一个临时变量中,并将该变量之前的所有变量都向前移动一位,直到达到变量的正确位置。这使得每个位置只需要进行1次值赋值。最后,将临时变量写入这个位置。

这样做的值赋值次数也少得多。

这不是最大的速度优势,但我认为它值得一提。

希望我表达清晰易懂,如果不是,请原谅,我不是英国本地人。


1
“然后将所有变量放在该位置的前面,向后移动一个位置。”这不也需要大量的赋值来移动数据吗?(假设数据是连续存储的,而不是链表) - Mark K Cowan
1
@MarkKCowan,没错,这就是插入排序每个“位置”都进行赋值的地方,正如上面的用户所说。基本上,插入排序可以在内部循环中用一个赋值来编写,而冒泡排序在内部循环中需要3个赋值。 - JSQuareD

10

插入排序的主要优点是它是一种在线算法。你不需要在开始时就拥有所有值。当处理来自网络或某些传感器的数据时,这可能非常有用。

我有一种感觉,这可能比其他传统的 n log(n) 算法更快。因为复杂度会是 n*(n log(n)) ,例如从流中读取/存储每个值 (O(n)),然后对所有值进行排序 (O(n log(n))),导致 O(n^2 log(n))

相反,使用插入排序仅需 O(n) 从流中读取值和 O(n) 将值放置到正确的位置,因此只需 O(n^2)。另一个优点是,您不需要缓冲区来存储值,可以在最终目的地中对它们进行排序。


如果允许数据的顺序遍历不仅仅是扫描数组,你可以边接收数据边将元素插入到二叉树中以更高效的方式进行动态排序。这样做可以使你在任何阶段都具有排序后的集合,所需要执行的总工作量为O(n log(n))(任何时候的中序遍历复杂度为O(m))。如果只需要最终有序结果,但希望将排序计算与数据传输重叠,则可使用堆排序(可以原地排序,类似于插入排序)。 - Peter Cordes
无论如何,对于问题规模足够大以至于O(f(n))复杂度类比实现细节和常数因子更重要的情况,冒泡排序和插入排序都不是理想的选择。 - Peter Cordes
更正:不适合这种情况。当您按排序顺序删除元素时,它会完成大部分排序工作,这就是为什么增长如此便宜的原因。这里的目标是在最后一个元素到达时完成大部分工作。 - Peter Cordes
无论如何,如果您确实需要为n个插入维护一个排序数组,那么它真正取决于哪种算法最适合对几乎排序的数组进行排序,其中顶部有一个未排序的元素。许多O(n log(n))排序算法在几乎排序的情况下是O(n),因此并不是真的需要sum(M=1..n, O(M * log(M)))工作量。那确实将是O(n^2 log(n)),但是通过选择正确的算法,它们将是总共O(n^2)的工作量。插入排序对此最有效。 - Peter Cordes

8
冒泡排序不支持在线排序(即在不知道有多少输入项的情况下无法对其进行排序),因为它实际上并没有跟踪已排序元素的全局最大值。当插入一个项目时,您需要从开头开始重新排序。

5

当有人需要从一大串数字中找出前k个元素时,冒泡排序比插入排序更好。

举例来说,在冒泡排序中,经过k轮迭代后,你就能得到前k个元素。但是在插入排序中,经过k轮迭代后,只能保证这k个元素已经排好序了。


2
尽管这两种方式的时间复杂度均为O(N^2), 但插入排序中的隐藏常数要小得多。这里的隐藏常数是指实际执行的原始操作数量。
何时使用插入排序的运行时间更好?
1. 数组几乎已排序 - 注意,在这种情况下,插入排序所执行的操作比气泡排序更少。 2. 数组相对较小:插入排序会移动元素以放置当前元素。只有在元素数量较少的情况下才优于气泡排序。
请注意,插入排序并不总是优于气泡排序。为了兼顾两者的优点,可以在数组较小的情况下使用插入排序,并在数组较大的情况下使用归并排序(或快速排序)。

2
如果元素数量不少,冒泡排序会更好吗?我的理解是,无论是插入排序还是冒泡排序,滑动或交换取决于所比较的元素是大于(IS)还是小于(BS),而不是元素数量。如果我理解有误,请指正。 - Asif

1

每次迭代中的交换次数

  • 插入排序在每次迭代中最多进行1次交换
  • 冒泡排序在每次迭代中可以进行0到n次交换。

访问和更改已排序部分

  • 插入排序访问(并在需要时更改)已排序部分,以找到正在考虑的数字的正确位置。
  • 当优化时,冒泡排序不访问已排序的内容。

在线或离线

  • 插入排序是在线的。这意味着插入排序在将适当的输入放置在其位置之前,一次只接受一个输入。它不必仅比较相邻输入
  • 冒泡排序不是在线的。它不是一次处理一个输入。它在每次迭代中处理一组输入(如果没有全部)。冒泡排序仅在每次迭代中比较并交换相邻输入

0

插入排序:

1.在插入排序中,不需要交换。

2.插入排序的时间复杂度为Ω(n)(最佳情况)和O(n^2)(最坏情况)。

3.与冒泡排序相比,插入排序较简单。

4.例如:将书籍插入图书馆,整理卡片。

冒泡排序:

1.在冒泡排序中需要交换。

2.冒泡排序的时间复杂度为Ω(n)(最佳情况)和O(n^2)(最坏情况)。

3.与插入排序相比,冒泡排序更为复杂。


1
为什么不需要交换?它会交换元素以将一个元素放到正确的位置。我不认为冒泡排序更复杂。 - parsecer

0

我会尝试给出比其他人更简洁和信息丰富的答案。

是的,在每次排序后,插入排序和冒泡排序直观上看起来相同 - 它们都在边缘构建一个排序子列表。

然而,插入排序通常会执行更少的比较。使用插入排序,我们仅在每次排序中对排序子列表进行线性搜索。对于随机数据,您可以预期进行m/2次比较和交换,其中m是排序子列表的大小。

使用冒泡排序,我们总是在未排序的子列表中比较每一对元素,因此这是n-m次比较(与插入排序在随机数据上相比多两倍)。这意味着如果比较昂贵/慢,则冒泡排序效果不佳。

此外,与插入排序相关的交换和比较的分支更可预测。我们同时进行线性搜索和线性插入,并且通常可以预测/假设线性搜索/插入将继续进行,直到找到正确的位置。使用冒泡排序,分支基本上是随机的,我们可以预期有一半的时间会出现分支错误!每次比较都是如此!这意味着如果比较和交换相对便宜/快速,则冒泡排序对流水线处理器不利。

这些因素使冒泡排序通常比插入排序慢得多。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接