各种排序和搜索算法的实现 ------ Python数据结构与算法第7章

1. 排序算法的稳定性

       排序算法(Sorting algorithm)是一种能将 一串数据依照特定顺序进行排列 的一种算法。稳定性:稳定排序算法会让原本有相等键值的纪录维持相对次序。也就是如果一个排序算法是稳定的,当有两个相等键值的纪录R和S,且 在原本的列表中R出现在S之前,在排序过的列表中R也将会是在S之前。 当相等的元素是无法分辨的,比如像是整数,稳定性并不是一个问题。然而,假设以下的数对将要以他们的第一个数字来排序:

(4,1) (3,1) (3,7) (5,6

在这个状况下,有可能产生两种不同的结果,一个是 让相等键值的纪录维持相对的次序,而另外一个则没有:

# 结果1:维持次序
(3,1) (3,7) (4,1) (5,6)
# 结果2:次序被改变
(3,7) (3,1) (4,1) (5,6)

      不稳定排序算法可能会在相等的键值中改变纪录的相对次序,但是稳定排序算法从来不会如此。不稳定排序算法可以被 特别地实现为稳定。作这件事情的一个方式是人工扩充键值的比较,如此在其他方面相同键值的两个对象间之比较(比如上面的比较中加入第二个标准:第二个键值的大小)就会被决定使用在原先数据次序中的条目,当作一个同分决赛。然而,要记住这种次序通常牵涉到额外的空间负担。

2. 常见排序算法效率比较

在这里插入图片描述

3. 冒泡排序

      冒泡排序(Bubble Sort)是一种简单的排序算法。它重复地遍历要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。冒泡排序最优时间复杂度:O(n),表示遍历一次发现没有任何可以交换的元素,排序结束。最坏时间复杂度:O(n2)。是一种比较 稳定 的排序算法。

第一种实现:

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


test_list = [9, 2, 3, 3, 1, 0]
bubble_sort(test_list)
print(test_list)
"""
[0, 1, 2, 3, 3, 9]
"""

第二种实现:

def bubble_sort(alist):
    n = len(alist)
    for i in range(n - 1,0,-1):
        for j in range(i):
            if alist[j] > alist[j + 1]:
                alist[j], alist[j + 1] = alist[j + 1], alist[j]
    return alist


test_list = [9, 2, 3, 3, 1, 0]
bubble_sort(test_list)
print(test_list)
"""
[0, 1, 2, 3, 3, 9]
"""

冒泡排序优化:

def bubble_sort(alist):
    n = len(alist)
    for i in range(n - 1, 0, -1):
        count = 0
        for j in range(i):
            if alist[j] > alist[j + 1]:
                alist[j], alist[j + 1] = alist[j + 1], alist[j]
                count += 1
        if count == 0:
            return
    return alist

test_list = [9, 2, 3, 3, 1, 0]
bubble_sort(test_list)
print(test_list)
"""
[0, 1, 2, 3, 3, 9]
"""
4. 选择排序

      选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

      选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对n个元素的表进行排序总共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种

      如果是排好序的,也需要n2次遍历才能确定,所以最优时间复杂度是O(n2),最坏时间复杂度也是O(n2)。选择排序是不稳定相同大小的元素,排序后前面的元素总会先被放到最后,后面的元素总会后被放到最后

def selection_sort(alist):
    n = len(alist)
    for i in range(n - 1):
        min_index = i
        for j in range(i + 1, n):
            if alist[j] < alist[min_index]:
                min_index = j
        # 判断当前最小值的索引是否还是i,如果是则i不用交换位置,否则需要交换位置
        if min_index != i:
            alist[i], alist[min_index] = alist[min_index], alist[i]


alist = [54, 226, 93, 17, 77, 31, 44, 55, 20]
selection_sort(alist)
print(alist)
"""
[17, 20, 31, 44, 54, 55, 77, 93, 226]
"""
5. 插入排序

      插入排序(Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中 从后向前扫描,找到相应位置并插入。插入排序在实现上,从后向前扫描过程中,需要 反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

      最优时间复杂度O(n),也就是已经排好序的情况下。最坏时间复杂度:O(n2),插入排序也是一种稳定的排序算法。
在这里插入图片描述

def insert_sort(alist):
    for i in range(1, len(alist)):
        for j in range(i, 0, -1):
            if alist[j] < alist[j - 1]:
                alist[j], alist[j - 1] = alist[j - 1], alist[j]
            """
            # 最优时间复杂度的情况
            else:
                break
            """

alist = [6, 5, 3, 1, 8, 7, 2, 4, ]
insert_sort(alist)
print(alist)
"""
[1, 2, 3, 4, 5, 6, 7, 8]
"""

使用while循环来实现,也是同样的道理:

def insert_sort(alist):
    for i in range(1, len(alist)):
        while i > 0:
            if alist[i] < alist[i - 1]:
                alist[i], alist[i - 1] = alist[i - 1], alist[i]
            # 最优时间复杂度的情况
            # else:
            #     break
            i -= 1

alist = [6, 5, 3, 1, 8, 7, 2, 4, ]
insert_sort(alist)
print(alist)
"""
[1, 2, 3, 4, 5, 6, 7, 8]
"""
6. 希尔排序

      希尔排序(Shell Sort)是插入排序的一种。也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止

      希尔排序的基本思想是:将数组列在一个表中并对列分别进行插入排序,重复这过程,不过每次用更长的列(步长更长了,列数更少了)来进行,最后整个表就只有一列了。
在这里插入图片描述

def shell_sort(alist):
    n = len(alist)
    # 如果是9个元素,则gap从4开始(不能确定取半才能是最优的)
    gap = n // 2
    # gap最小缩小为1,也就是不能大于0
    while gap > 0:
        for i in range(gap, n):
            for i in range(i, 0, -gap):
                if alist[i] < alist[i - gap]:
                    alist[i], alist[i - gap] = alist[i - gap], alist[i]
            """
            # 与内层的for循环是等价的
            while i > 0:
                if alist[i] < alist[i - gap]:
                    alist[i], alist[i - gap] = alist[i - gap], alist[i]
                i -= gap
            """

        # 缩短gap
        gap //= 2

alist = [54, 26, 93, 17, 77, 31, 44, 55, 20]
shell_sort(alist)
print(alist)
"""
[17, 20, 26, 31, 44, 54, 55, 77, 93]
"""

希尔排序的最优时间复杂度是 根据步长序列的不同而不同。最坏时间复杂度:O(n2),也就是 当gap取1的时候就是普通的插入排序。 相同的值在不同的分组中排序,最后合并分组,不能保证这两个相同值顺序还是和之前一致,所以这是不稳定的排序算法!

7. 快速排序

       快速排序(Quicksort),又称 划分交换排序(partition-exchange sort)。通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。步骤:

① 从数列中挑出一个元素,称为"基准"(pivot)

② 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。

③ 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
在这里插入图片描述
       递归的最底部情形是数列的大小是0或1,也就是 永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代中,它至少会把一个元素摆到它最后的位置去。

def quick_sort(alist, first, last):
    # 递归结束条件
    if first >= last:
        return
    # 基准值
    pivot_value = alist[first]
    # 每次传入的索引位置:first和last都是不同的
    low_pointer = first
    high_pointer = last
    while low_pointer < high_pointer:
    	# 当列表中高索引位置的元素大于或者等于基准值时,只需要索引-1,也就是索引移动到上一个元素
        while low_pointer < high_pointer and alist[high_pointer] >= pivot_value:
            high_pointer -= 1
        # 当存在小于基准值的元素,while循环停止,索引移动停止,把这个索引放到低索引位置
        alist[low_pointer] = alist[high_pointer]
        # 当列表中低索引位置的元素小于基准值时,只需要低索引+1,也就是移动到下一个元素
        while low_pointer < high_pointer and alist[low_pointer] <= pivot_value:
            low_pointer += 1
        # 当存在大于基准值的元素,while循环停止,索引移动停止,把这个索引放到高索引位置
        alist[high_pointer] = alist[low_pointer]
    # low_pointer也可以换成high_pointer,因为这个时候low_pointer与high_pointer是等价的
    alist[low_pointer] = pivot_value
    # low_pointer左边的列表进行快速排序
    quick_sort(alist, first, low_pointer - 1)
    # low_pointer右边的列表进行快速排序
    quick_sort(alist, low_pointer + 1, last)

alist = [54, 26, 93, 17, 77, 31, 44, 55, 20]
quick_sort(alist, 0, len(alist) - 1)
print(alist)
"""
[17, 20, 17, 17, 31, 54, 17, 17, 93]
"""

      最优时间复杂度:O(nlogn),最坏时间复杂度:O(n2),每次分区会有一个单独的元素。快速排序是不稳定性的排序算法。从一开始快速排序平均需要花费O(n log n)时间的描述并不明显。但是不难观察到的是分区运算,数组的元素都会在每次循环中走访过一次,使用O(n)的时间。在使用结合(concatenation)的版本中,这项运算也是O(n)。

       在最好的情况,每次我们运行一次分区,我们会把一个数列分为两个几近相等的片段。这个意思就是每次递归调用处理一半大小的数列。因此,在到达大小为一的数列前,我们只要作log n次嵌套的调用。这个意思就是调用树的深度是O(log n)。但是在同一层次结构的两个程序调用中,不会处理到原来数列的相同部分。因此,程序调用的每一层次结构总共全部仅需要O(n)的时间(每个调用有某些共同的额外耗费,但是因为在每一层次结构仅仅只有O(n)个调用,这些被归纳在O(n)系数中)。结果是这个算法仅需使用O(n log n)时间。

8. 归并排序

       归并排序是采用 分治法 的一个非常典型的应用。归并排序的思想就是 先递归分解数组,再合并数组。将数组分解最小之后,然后合并两个有序数组,基本思路是比较两个数组的最前面的数,谁小就先取谁,取了后相应的指针就往后移一位。然后再比较,直至一个数组为空,最后把另一个数组的剩余部分复制过来即可。
在这里插入图片描述

def merger_sort(alist):
    # 每次都要获取分组后列表的长度
    n = len(alist)
    # 根据列表的长度分组
    mid_elem = n // 2
    # 当列表只有一个元素时无法分组就直接返回这个列表
    if n <= 1:
        return alist
    # 分组后左边的元素形成的新列表
    left_elems = merger_sort(alist[:mid_elem])
    # 分组之后右边的元素形成的新列表
    right_elems = merger_sort(alist[mid_elem:])
    # 左右列表分别设置一个指针
    left_pointer, right_pointer = 0, 0
    # 新的列表用于存放需要合并的且排序好的左右子列表
    ret = []
    # 只要指针的值小于左右子列表的长度,需要继续排序
    while left_pointer < len(left_elems) and right_pointer < len(right_elems):
        # 如果左子列表的元素小于或者等于右子列表则将左子列表的该元素添加到新列表中,指针向后移动1次
        if left_elems[left_pointer] <= right_elems[right_pointer]:
            ret.append(left_elems[left_pointer])
            left_pointer += 1
        # 如果右子列表的元素小于或者等于左子列表则将右子列表的该元素添加到新列表中,指针向后移动一次
        else:
            ret.append(right_elems[right_pointer])
            right_pointer += 1
    # 剩下的元素直接追加到列表后面,比如1,2,3,4排好1,2剩下的3,4直接追加到列表中
    # 这里由于执行效率的原因不建议使用列表相加
    ret.extend(left_elems[left_pointer:])
    ret.extend(right_elems[right_pointer:])
    return ret


alist = [54, 26, 93, 17, 77, 31, 44, 55, 20]
sorted_alist = merger_sort(alist)
print(sorted_alist)
"""
[17, 20, 26, 31, 44, 54, 55, 77, 93]
"""

      最优时间复杂度:O(nlogn),最坏时间复杂度:O(nlogn),归并排序是一种稳定的排序算法。归并排序虽然在时间复杂度上要比其它排序算法减小了,但是 空间复杂度却增加了,因为新增了新的列表,占用了更多的内存空间。

9. 搜索算法

      搜索是在一个项目集合中找到一个特定项目的算法过程,搜索通常的答案是真的或假的。 搜索的几种常见方法:顺序查找、二分法查找、二叉树查找、哈希查找。

10. 二分法查找

      二分查找又称折半查找,优点:比较次数少,查找速度快,平均性能好;其缺点:要求待查表为有序表,且插入删除困难。因此,折半查找方法适用于不经常变动而查找频繁的有序列表。首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。二分查找只能作用于有序的顺序表。
在这里插入图片描述

11. 二分法查找的两种实现

不使用递归实现:

def binary_search(alist, item):
    first_index = 0
    end_index = len(alist) - 1
    while end_index >= first_index:
        middle_index = (end_index + first_index) // 2
        if item == alist[middle_index]:
            return True
        elif item < alist[middle_index]:
            end_index = middle_index - 1
        else:
            first_index = middle_index + 1
    return False


test_list = [0, 1, 2, 8, 13, 17, 19, 32, 42, ]
print(binary_search(test_list, 3))
print(binary_search(test_list, 17))
"""
False
True
"""

使用递归实现:

def binary_search(alist, item):
    len_alist = len(alist)
    if len_alist == 0:
        return False
    else:
        middle_index = len_alist // 2
        if item == alist[middle_index]:
            return True
        elif item < alist[middle_index]:
            return binary_search(alist[:middle_index], item)
        else:
            return binary_search(alist[middle_index + 1:], item)


test_list = [0, 1, 2, 8, 13, 17, 19, 32, 42, ]
print(binary_search(test_list, 3))
print(binary_search(test_list, 17))
"""
False
True
"""

最优时间复杂度:O(1),最坏时间复杂度:O(logn)

Github:https://github.com/ThanlonSmith/Data-Structure-Python3

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章