学习笔记-简单的排序算法(Python实现)

1、插入排序

如图所示,从第1位开始不断地与前面的元素挨个比较,然后将较大的元素交换到后面,较小的元素交换到前面,直到所有元素排好序为止。


def insert_sort(lst):
    for i in range(1, len(lst)):
        x = lst[i]
        temp = i
        while temp > 0 and lst[temp-1] > x:
            lst[temp] = lst[temp-1] #只要前一个元素大于后一个元素,就将较大的元素交换到后面
            temp -= 1
        lst[temp] = x
    return lst

if __name__=="__main__":
    lst = [4,1,6,8,34,9,2,5]
    print(insert_sort(lst))

算法的空间复杂度为O(1),时间复杂度为O(n2).


2、直接选择排序

每次都顺序扫描出未排序序列中最小的元素,然后将其添加在已排序序列的末尾。

def select_sort(lst):
    for i in range(len(lst)-1): #只需要比较len(lst)-1次,剩余的最后一个元素肯定是最大的
        min_position = i  # min_position记录最小元素的位置
        for j in range(i+1, len(lst)):
            if lst[j] < lst[min_position]: #如果有更小的元素,则更新min_position
                min_position = j
        if i != min_position: #有可能位置i已经是最小元素的位置,所以此时就不需要交换了
            lst[i], lst[min_position] = lst[min_position], lst[i]
    return lst

if __name__=="__main__":
    lst = [4,1,6,8,34,9,2,5]
    print(select_sort(lst))

算法的空间复杂度为O(1),时间复杂度为O(n2).

直接选择排序的实际排序效率低于插入排序。所以直接选择排序很少被实际应用。

将原来已排序序列的后一个元素交换到最小元素的位置,有可能出现两个相同的元素做交换,这说明算法不稳定。

延伸:

直接选择排序比较低效,是因为在做顺序扫描时,每次都需要从头做一次完整的比较,这个过程中做了很多重复的工作。要想克服这个缺点,就应该设法记录比较之后的获得的信息。利用数的结构来记录这种信息,由此出现了堆排序: 图解堆排序。但是堆排序也可能出现两个相同元素被交换的情况。所以算法也具有不稳定性。堆排序的时间复杂度为O(nlog n),空间复杂度是O(1)。


3、交换排序:冒泡排序
一个未排序序列中,肯定存在逆序对(即前一个元素大于后一个元素),通过不断交换逆序对中两个元素的位置,最终将得到排序序列。


上图中的排序过程中,每一遍检查都可以将最大的元素交换到最后面。这就像水中的气泡浮起,也是这种算法名字的由来。

当序列中最小元素在序列的最后时,冒泡排序需要n-1遍扫描。在一般情况下,并不需要这么多次。只要在某次扫描中没有发现逆序对,则说明排序已经完成了。

def bubble_sort(lst):
    for i in range(len(lst)-1): #最多需要比较len(lst)-1次
        found = False #found变量初始为false,如果存在逆序对,则置为True,如果不存在,则为false,则会控制循环提前结束
        for j in range(1, len(lst)-i): #第一次需扫描n个元素,第二次需扫描n-1个元素,依此类推
            if lst[j-1] > lst[j]: #如果存在逆序对,则交换元素
                lst[j-1], lst[j] = lst[j], lst[j-1]
                found = True
        if not found:
            break
    return lst

if __name__=="__main__":
    lst = [30,13,25,16,47,26,19,10]
    print(bubble_sort(lst))

如果初始序列有序,则只需要扫描一次就结束了,时间复杂度是O(n)。

冒泡排序最坏的时间复杂度是O(n2),平均时间复杂度也是O(n2),最好情况下的时间复杂度是O(n)。其也是一种原位排序算法,空间复杂度是O(1)。

冒泡排序效率低于插入排序。一是因为反复交换相邻元素,累加起来代价较大;二是一些距离最终位置较远的元素会拖累整个算法。

此外,还有一种交错起泡的算法,可以将小元素快速地移向左方。从左向右扫描一次,再从右向左扫描一次,交替进行。

只需要 4遍就可以完成以下的排序工作。


4、快速排序

快速排序算法在实践中是平均速度最快的算法之一。

基本思想是:从序列中选一个元素作为‘标准’,将序列中剩余的元素与这个标准一一比较,小于‘标准’的元素放在其左边,大于‘标准’的元素放在其右边;然后在两个子序列中按照同样的方式递归地划分下去。

算法实现:


取序列中第一个元素作为‘标准’,记为R。小于R的元素在其左边,大于R的元素在其右边,中间是尚未检查的元素。此外,下标i和j分别指向未检查元素序列的第一个位置和最后一个位置(i初始时指向整个序列的第一个元素)。然后交替进行如下操作:

(1)从位置j开始向左逐个比较当前元素与R的大小,直至找到第一个小于R的元素,将其存入位置i。然后i加1,指向下一个需要检查的元素。

(2)从位置i开始向右逐个比较当前元素与R的大小,直至找到第一个大于R的元素,将其存入位置j。

重复以上两步,直到i与j相等,此时将R存入i=j的这个位置,一次划分完成。

def quick_sort(lst):
    record = qsort_rec(lst, 0, len(lst)-1) #i和j分别初始化为序列的第一个和最后一个位置
    return record

def qsort_rec(lst, l, r):
    if l >= r:    #说明序列中没有元素或者只有一个元素
        return
    i, j = l, r
    standard = lst[i] #标准元素为序列中最左边的元素
    while i < j:      #终止时i=j
        while i < j and lst[j] >= standard: #从右向左找到第一个小于标准的元素
            j -= 1
        if i < j:      #将上面找到的元素放到位置i
            lst[i] = lst[j]
            i += 1
        while i < j and lst[i] <= standard: #从左向右找到第一个大于标准的元素
            i += 1
        if i < j:      #将上面找到的元素放到位置j
            lst[j] = lst[i]
            j -= 1
    lst[i] = standard
    qsort_rec(lst, l, i-1)
    qsort_rec(lst, i+1, r)
    return lst


if __name__=="__main__":
    lst = [30,13,25,16,47,26,19,10]
    print(quick_sort(lst))

如果每次划分都能讲序列分成两个基本相等的子序列,那么整个序列将被分为大约log n层;在其中一层中,元素的比较次数不会超过序列的长度n,所以快速排序的平均时间复杂度是O(log n)。

但是,如果每层划分得到的两个子序列中总有一个为空,另一个子序列中的元素个数只比本层划分前少一个,这样需要分为n-1层,每层的比较次数从n-1逐层减少到1,。此时是快速排序最坏的时间复杂度O(log n)。

其空间复杂度最坏时是O(n),但是可以通过不同的实现方式,提升至O(log n)。


5、归并排序

基本过程:

  • 首先,将待排序序列中的n个元素看作n个有序子序列,每个子序列长度为1。
  • 然后,将当前序列中的有序子序列两两归并,完成一遍后整个序列中的已排序序列个数减半,每个子序列长度翻倍。
  • 对长度翻倍后的子序列继续两两归并,最后将得到一个长度为n的有序序列。

归并排序适合处理存储在外存中的大量数据。

下图是一个例子:第一遍将序列归并为一组长度为2的有序序列,最后的元素44没有归并对象,原样留到下一步;第二遍归并出3个长度为4的有序序列;第三遍只能归并出一个长度为8的有序序列,剩余的3个元素原样留到下一步;最后一步便已经得到了排序序列。


#实现一对有序序列的归并操作,将归并的结果存入另一个顺序表的相同位置。
def merge(lfrom, lto, low, mid, high):
    i, j, k = low, mid, low
    while i < mid and j < high:#每次都将两个子序列中最小的元素加入到lto中,但是总是会有某个序列的后面会剩下几个元素,需要下面的两个循环再将这些元素加到lto中
        if lfrom[i] <= lfrom[j]:
            lto[k] = lfrom[i]
            i += 1
        else:
            lto[k] = lfrom[j]
            j += 1
        k += 1

    while i < mid: #将第一个子序列的剩余元素加入到lto中
        lto[k] = lfrom[i]
        i += 1
        k += 1

    while j < high: #将第二个子序列的剩余元素加入到lto中
        lto[k] = lfrom[j]
        j += 1
        k += 1
    return lto

#其中的某一遍归并操作,将表中的所有元素进行一遍归并
def merge_pass(lfrom, lto, list_len, sub_len): #list_len为整个序列的长度,sub_len为子序列的长度
    i = 0
    while i + 2*sub_len <= list_len: #处理序列中长度为sub_len的两个子序列
        merge(lfrom, lto, i, i+sub_len, i+2*sub_len)
        i += 2*sub_len
    if i + sub_len < list_len: #此时剩下两个子序列,但只有第一个子序列的长度满足sub_len
        merge(lfrom, lto, i, i+sub_len, list_len)
    else:                      #否则,此时只剩下一个子序列,直接将这个子序列添加在lto后面
        for j in range(i, list_len):
            lto[j] = lfrom[j]

#主函数,分配不同长度的sub_len,调用归并函数
def merge_sort(lst):
    sub_len, list_len = 1, len(lst)
    temp_lst = [None] * list_len
    while sub_len < list_len: #不断将sub_len的长度翻倍
        merge_pass(lst, temp_lst, list_len, sub_len)
        sub_len *= 2
        merge_pass(temp_lst, lst, list_len, sub_len)
        sub_len *= 2

if __name__=="__main__":
    lst = [30,13,25,16,47,26,19]
    merge_sort(lst)
    print(lst)
算法的时间复杂度为O(nlog n),空间复杂度为O(n)



猜你喜欢

转载自blog.csdn.net/qq_34840129/article/details/80731347