排序算法:快排 二分 冒泡_基本算法:冒泡排序

排序算法:快排 二分 冒泡

欢迎回到基本算法,在这里我将介绍每个程序员都应该了解和理解的许多不同算法。 如今的算法是非常简单但效率低下的Bubble Sort。

冒泡排序算法

再一次,我们将从需要排序的未排序数组开始:

arr = [5 , 4 , 3 , 2 , 1 ]

简单地说,冒泡排序只是将数组传递过来,将一个数字与旁边的数字进行比较。 如果左边有较大的数字,则交换他们的位置。 它逐个遍历每个元素,直到完成数组为止,然后重复多次以对数组进行排序。 乍一看,这似乎很简单,而且似乎没有更多内容,但深入了解可以进一步了解我们如何实现和优化此算法。

让我们以气泡排序这个名字开始。 之所以将其称为“冒泡排序”,是因为它会导致最大的未排序数字在第一遍“冒泡”到数组顶部。 因为如果较大的数字在左边,它将始终交换2个数字的位置,而最大的数字将始终在左边,直到最后它就位,然后逻辑上最大的数字将与前面的每个数字交换在第一次经过并正确放置。 如果我们理解了这一点,便可以理解,经过一次之后,最大的要素肯定就到位了。 第二次越过之后,第二个最大的元素就位,依此类推。

这意味着长度为n的数组将按n次遍历该数组。 这也意味着我们可以通过跟踪数组被排序的次数来确定在数组中准确放置了多少个数字。 知道了这一点,我们也不必每次都遍历整个数组。 每次遍并不需要遍历所有n个元素,而是遍历n-(我们遍历数组的次数) ,因为之后的所有元素都已经排序。 因此,该算法可以分为以下两个步骤:

  1. 对于数组中的每个数字,必须完成数组的传递,并执行交换
  2. 对于数组中的每一遍,比较当前数和下一遍,如果左边较大,则交换它们的位置,但只对数组中未排序的部分进行。

现在,让我们通过一个示例逐步介绍该算法。 这是我们开始的数组:

[5, 4, 3, 2, 1]

我们有5个数字,因此我们在上面的第一个循环中做了5次。

JavaScript:var arr = [ 5 , 4 , 3 , 2 , 1 ]

const bubble_sort = ( function ( unsorted ) {
    for ( var i= 0 ; i<unsorted.length;i++){
        // Do the second loop
    }
})
Python:

arr = [5 , 4 , 3 , 2 , 1 ]

def bubble_sort (unsorted) :
    for i in range(len(unsorted)):
        # Do the second loop

请注意,在第一个循环中,我们实际上不需要访问数组本身,并且该循环的索引不会指向数组中的任何位置,但它是我们用来跟踪已执行多少次的数组遍历了数组,因此,已经排序了多少个项目。

设置下一个循环需要我们思考一下在完成之前需要在数组中停止的位置。 如果我们有5个项目,则最后一个项目位于arr[4]

第二个循环使我们将当前索引的数字与下一个索引的数字进行比较,因此我们必须在结束之前停止操作(因为尝试访问不存在的arr[5]元素将导致问题)。

为了比较所有项目,我们需要在要触摸的最后一个项目之前停止在索引处运行循环。 我们还知道,第一次运行循环时(当i = 0时 ),绝对不会对所有项目进行排序,这意味着我们必须遍历所有项目。 第二次运行循环i = 1时 ,将对最后一项进行排序,因此我们可以忽略最后一项。 因此,内部循环需要运行n-(1 + i)次。

JavaScript:var arr = [ 5 , 4 , 3 , 2 , 1 ]

const bubble_sort = ( function ( unsorted ) {
    for ( var i= 0 ; i<unsorted.length;i++){
        for ( var j= 0 ; j < unsorted.length-( 1 +i);j++){
            // compare this number to the next one
        }
    }
})
Python: 

arr = [5 , 4 , 3 , 2 , 1 ]

def bubble_sort (unsorted) :
    for i in range(len(unsorted)):
        for j in (range(len(unsorted) - (i+ 1 ))):
            # Do comparisons

现在,我们要做的就是比较每个j索引处的左项和右项,并在适当时交换它们。 使用python中的一行可以有效地完成此操作,但是JavaScript要求我们将其中一个值临时存储在变量中,将一个值复制到另一个中,然后复制存储的变量。 我们最终的基本功能如下所示:

最佳化

从这里可以进一步优化气泡排序。 我记得我第一次尝试编写排序算法,却一无所知,并且写了类似递归伪冒泡排序的内容,看起来很像这样:

def bubble_sort (unsorted) :
    swapped = False
    for i in range(len(unsorted) -1 ):
        if unsorted[i] > unsorted[i+ 1 ]:
            unsorted[i],unsorted[i+ 1 ] = unsorted[i+ 1 ],unsorted[i]
            swapped = True
    if swapped == True :
        return bubble_sort(unsorted)
    else :
        return unsorted

这以非常相似的方式工作,只是没有嵌套循环。 它遍历整个数组,直到被排序为止。 这样做的好处是,您可以利用遍历数组的方式来验证数组是否已经排序,并且可以在排序时停止,从而减少了执行的步骤数。 缺点之一是它无法跟踪遍历数组的次数,因此它不知道在正确的位置有多少个项目以及它可以跳过多少个项目,这加起来又很复杂随着时间的流逝,大量不必要的执行。 (我们不会过多介绍的另一个缺点是O(n ^ 2)函数的递归性质,该函数会很快使堆栈大小变得太大,并导致程序挂起或崩溃。)

优化它(尽管几乎肯定是徒劳的,我们将在下面观察到)可以是一个简单的问题,即将上述功能之一的缺失优势整合到另一个功能中。 如果我们从带有嵌套循环的函数开始,那么我们可以跟踪每次通过,并且如果我们在任何时候都停止交换数字,我们就会知道我们已经对整个数组进行了排序,并且可以返回我们现在拥有的东西。 因此,可以使用以下方法优化我们第一个完成的(python)函数,使其看起来像这样:

arr = [5 , 4 , 3 , 2 , 1 ]

def bubble_sort (unsorted) :
    for i in range(len(unsorted)):
        swapped = False
        for j in (range(len(unsorted) - (i+ 1 ))):
            if unsorted[j] > unsorted[j+ 1 ]:
                unsorted[j],unsorted[j+ 1 ] = unsorted[j+ 1 ],unsorted[j]
                swapped = True
        if swapped == False :
            return unsorted
    return unsorted

现在,优化递归气泡排序函数仅需要我们跟踪它被调用了多少次,这可以通过添加传递给该函数的变量并在每次调用该函数时增加1来轻松实现。

然后,我们可以不使用range(len(unsorted)-1) ,而是将相减后的1变成(1+the number of sorted items)

第二个功能经过第一个功能的优化,如下所示:

def bubble_sort (unsorted, last_sorted) :
    swapped = False
    for i in range(len(unsorted)-( 1 +last_sorted)):
        if unsorted[i] > unsorted[i+ 1 ]:
            unsorted[i],unsorted[i+ 1 ] = unsorted[i+ 1 ],unsorted[i]
            swapped = True
    if swapped == True :
        return bubble_sort(unsorted, last_sorted+ 1 )
    else :
        return unsorted

bubble_sort(arr, 0 )

时间复杂度和效率

那么,冒泡排序算法的时间复杂度是多少,为什么我一直在引用它的严重性呢? 由于通常将其描述为O(n ^ 2) ,这意味着对于8个数组,我们必须执行64个步骤,对于9个数组,我们必须执行81个步骤,对吗? 好吧,实际上没有。 所以,如果没有,多少步没有考虑?

Stack Overflow似乎有多个相似但不同的方程作为答案,为什么有人说n *(n-1),而另一些人说n *(n-1)/ 2 ? 而且,知道了这一点, 为什么我们说需要O(n ^ 2)时间呢?

为了开始探索这些问题的答案,让我们写出最无效的编写该算法的方法。

def bad_bubble_sort (unsorted) :
    for i in range(len(unsorted)):
        for j in range(len(unsorted) -1 ):
            if unsorted[j] > unsorted[j+ 1 ]:
                unsorted[j],unsorted[j+ 1 ] = unsorted[j+ 1 ],unsorted[j]
    return unsorted

此实现不检查数组是否已排序,也不知道数组中已经排序了多少个数字。 这意味着它可以根据设计运行最大次数。 外循环需要对n个项目进行排序,因此它将运行n次。 在内部循环(由外部循环调用)中,循环的每一步都将数组中当前指向的项与下一个进行比较。

这意味着除最后一个元素外,所有其他元素都与下一个元素配对,这使我们可以在长度为n的任何数组中比较n-1对。 因此,我们可以计算出最差实现的性能实际上可以精确地计算为O(n *(n-1)) 。 这接近n ^ 2 ,但不完全相同。

那么,我们制作的第一个版本会跟踪最后排序的项目数呢? 毕竟,外循环保持不变,但内循环变小! 为了计算这一点,我们需要做更多的数学运算。 对于数字n ,即数组的长度,我们知道第一次通过整个数组时,我们执行n-1个比较。 下次,我们执行n-2 ,然后执行n-3 ...直到我们下降到1。

因此,如果我们有10个项目,我们将进行9个比较,然后进行8个比较,这样9 + 8 + 7 + 6 + 5 + 4 + 3 + 2 + 1 = 45。 这是一个序列和,可以用数学方式描述为( n *(n-1))/ 2 。 将10插入那里,我们得到10 *(10-1)/ 2 = 10 * 9/2 = 90/2 =45。( 在这里您可以找到对此的更好解释 )。

这意味着跟踪排序项目数的实现将执行比不执行的愚蠢步骤少一半的步骤,因此速度快了一倍。

使用这两个函数,我们现在可以比较它们随着 n 的大小 变大而产生的结果,这就是Big-O表示法的全部含义。 在非常实现的bad_bubble_sort()函数中,其实际复杂度计算为n *(n-1) ,如果传递10个项目的数组,则需要进行90次比较。 如果我们通过20个项目,则需要进行380个比较。 如果我们通过5项,则需要进行20次比较。 实际上,很容易看到从5项增加到10项,将输入加倍,将获得4.5倍的输出。

当我们从10项增加到20项时,我们的产出是4.22倍。 随着数组大小加倍,我们将继续创建4倍的输出,而始终保持略高于输出。

常规的bubble_sort()函数以相同的方式工作。 如果传递该函数,并使用计算出的(n *(n-1))/ 2复杂度,则可以插入5并得到结果10。可以插入10项,得到结果45。再次,跳转从5件增加到10件,输出提高了4.5倍。 这意味着我们可以观察到两个函数以完全相同的方式成比例地增加其输出。

因此,尽管功能不同,但随着 输入大小的增加 ,它们的复杂度也以完全相同的速率增长

我们还可以观察到作为输入的大小为O(n *(N-1))保持被加倍,则输出的尺寸趋于靠拢朝向四倍,或我们可以说 m 的因子增加 ,输出趋向于 m ^ 2 。 我们可以轻松地通过较大的值验证这一点:插入1,000可得到999,000的输出。 输入的三倍将使输出增加近9倍,插入3,000则使我们获得8,997,000,增长了9倍多。

正因为如此,在讨论气泡排序的性能时,我们称气泡排序的复杂度为O(n ^ 2) 。 我们通常对实际计算执行该算法所需的步骤数量通常不感兴趣,但是我们希望能够解释随着输入大小的增加,运行时间如何增长 ,即O( n ^ 2)

虽然我们已经讨论了平均和最坏情况下的复杂性,但我想简单地谈一下最佳情况下的复杂性。 绝对最佳的情况是O(n) ,其精确度为O(n-1) 。 其原因在于优化策略,该策略跟踪我们是否执行了任何交换。 如果我们无需进行一次交换就可以遍历整个未排序的数组,那么我们知道我们有一个已排序的数组。

如果传递了已经排序的数组,并且我们已经实现了此优化,则只需对数组进行一次遍历(进行n-1个比较)。 当然,所需的时间完全取决于n的大小,因此我们说最好的情况是O(n) 。 但是,通过观察上面的平均情况和最坏情况,我们可以看到,对于任何可观大小的列表,冒泡排序都不是一个好主意,并且对于预期已经进行了大多数排序的列表,效果最好。

结论

冒泡排序是每个程序员都应该知道的算法的基本基础之一。 尽管基本但效率低下,但它很直观,是大多数人在没有任何事先学习或经验的情况下尝试编写排序算法时首先想到的一种基本排序方法。 生产中的这种简单性和缺乏实际应用的情况不应被视为不学习或研究它的原因。

实际上,这种非常简单的方法使其成为演示优化基础的理想算法,是在递归中选择嵌套循环的原因(尤其是当您可以动态减小一个循环的大小时),并且是通常证明时间复杂度的好方法以及各个算法的特定时间复杂度。

希望,如果您已经读了那么多文章,那么您将获得一些见识并学到一两个东西。 如果您喜欢这个,请继续分享! 如果您有任何要让我涵盖和探索的主题,请随时与我联系并给我发送电子邮件! 最重要的是,继续编码和练习,直到下一次!

翻译自: https://hackernoon.com/essential-algorithms-the-bubble-sort-2v4j3ydg

排序算法:快排 二分 冒泡

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值