排序与搜索

排序

排序算法的稳定性

稳定,即对于相等的元素,排序算法不会改变其原有的次序。

如果被排序的数是整数,稳定不稳定并不重要。但是如果是一个序列、集合或者映射,要按照他们的某一个元素进行排序,那么对于相等元素保持原有次序或许非常重要。

在这里插入图片描述

冒泡排序

最简单,最符合常规思路。

思路

每一轮都把最大的那个沉到最下面。

由于剩下的最后一轮只有一个元素,故而只需进行n-1轮,每轮比较n-1次
当然你要进行n轮也行。但是每轮最多是n-1次,防止j+1越界。

二重循环:
外层记录轮数
内层进行一轮下沉操作

实现

# 冒泡
# 最坏O(n^2)
# 最优O(n)
# 稳定
def bubble_sort(lst):
    for i in range(len(lst) - 1):
        for j in range(len(lst) - 1 - i):
            if lst[j] > lst[j + 1]:
                lst[j], lst[j + 1] = lst[j + 1], lst[j]


# 对冒泡进行最优化:
# 对有序序列不做处理:
def op_bubble_sort(lst):
    for i in range(len(lst) - 1):
        count = 0
        for j in range(len(lst) - 1 - i):
            if lst[j] > lst[j + 1]:
                lst[j], lst[j + 1] = lst[j + 1], lst[j]
                count += 1
        if count == 0:
            break
            #return

选择排序

与冒泡排序思路几乎相同

又不稳定,效率也不高,用得很少。

思路

二重循环:
外层:记录轮数,并将找到的最大值沉底(最小值上浮)
内层:找到最大的(最小)的

下沉:
进行N-1轮
每轮(第i轮)挑选出序列中未排序部分中最大的那个元素与下标为N-i的元素交换位置(无序序列中最后一个)
在列表尾部形成有序数列

上浮:
进行N-1轮
每轮(第i轮)挑选出最小的那个元素,与下标为i的元素(无序序列中的首元素)交换位置。
在列表头部形成有序数列

实现

# 选择排序
# 最坏O(n^2)
# 最优O(n^2):并不能通过一次循环判断出序列是否是有序的。
# 不稳定
def selection_sort(lst):
    for i in range(len(lst) - 1):
        max = lst[0]
        idx = 0
        # 如果从0开始会多比较一次,其实也没什么
        for j in range(1, len(lst) - i):
            if lst[j] > max:
                max = lst[j]
                idx = j
        # j是子序列最后一个元素下标
        if lst[idx] != lst[j]:
            lst[idx], lst[j] = lst[j], lst[idx]


def selection_sort2(lst):
    for i in range(len(lst) - 1):
        min = lst[i]
        idx = i
        for j in range(i + 1, len(lst)):
            if lst[j] < min:
                min = lst[j]
                idx = j
        # i是子序列首元素下标
        if lst[idx] != lst[i]:
            lst[i], lst[idx] = lst[idx], lst[i]


# 另一种更为精炼的写法
# 上浮:
def selection_sort3(lst):
    for i in range(len(lst) - 1):
        min_index = i
        for j in range(i + 1, len(lst)):
            if lst[j] < lst[min_index]:
                min_index = j
        if min_index != i:
            lst[i], lst[min_index] = lst[min_index], lst[i]


# 下沉:
def selection_sort4(lst):
    for i in range(len(lst) - 1):
        max_index = 0
        for j in range(1, len(lst) - i):
            if lst[j] > lst[max_index]:
                max_index = j
        if max_index != j:
            lst[j], lst[max_index] = lst[max_index], lst[j]

插入排序

在列表头部逐渐形成有序数列。

思路

将首元素作为第一个有序数列,将后面的1到n-1个数插到前面。
进行N-1轮
从第二个元素(下标为1)开始,每轮将元素插入到有序数列的应有位置上。

二重循环
外层记录轮数(需要插入的数字个数)
内层进行插入操作
插入方法:
与它前面的数比较,如果比自己大,就一直交换下去(倒着来的)
需要注意的是每次交换过后,插入数的下标会改变

实现

# 插入排序
# 最坏O(n^2)
# 最优O(n)
# 稳定
def insert_sort0(lst):
    # lst[i]是每轮插入的数,i同时也是有序数列长度
    for i in range(1, len(lst)):
        # 倒着与有序数列中的数进行比较
        # 注意每次交换过后,插入的数在序列中的下标会改变
        swap_index = i
        for j in range(i - 1, -1, -1):
            if lst[swap_index] < lst[j]:
                lst[swap_index], lst[j] = lst[j], lst[swap_index]
                swap_index = j


# 阿西,刚才那个实现的真是啰嗦,我每次都用j不就可以了吗!
# 就不用再设置swap变量了,因为对于每次lst[j]不就是要插入的元素吗!!!
def insert_sort(lst):
    for i in range(1, len(lst)):
        for j in range(i, 0, -1):
            if lst[j] < lst[j - 1]:
                lst[j], lst[j - 1] = lst[j - 1], lst[j]

#最优化处理:
def op_insert_sort(lst):
    for i in range(1, len(lst)):
        for j in range(i, 0, -1):
            if lst[j] < lst[j - 1]:
                lst[j], lst[j - 1] = lst[j - 1], lst[j]
            #因为前面是有序数列,如果大于等于最后一个,那肯定大于所有,就不用进行这一轮了。
            else:
                break

希尔排序

是插入排序的改进版。又称“缩小增量排序”或“递减增量排序”。由Dr.Shell在1959年提出。

基于插入排序在“当序列基本有序时效率接近线性”的特点,以及插入排序每次只能移动一个元素的缺陷对插入排序进行改进。

思路

每个gap都将原序列按照gap步长打断为gap个子序列。

每个序列的最大长度是len//gap+1,一般长度为:len//gap。

lst[0],lst[1],……,lst[gap-1] 分别是每个序列的首元素。

将每次gap下的所有子序列分别进行直接插入排序。

当gap很大时,每个子序列长度很小,可以认为序列基本有序。所以很高效。

每取一个gap排序一次,整个序列都会更接近“基本有序”。

gap折半递减。

就这样高效地轮巡下去……

最终得到一个有序数列。

实现

# 希尔排序。改进版插入排序。
# 非吾等凡人能想出来的方法。
# 最优:取决于步长,可以达到O(n^1.3)
# 最坏:O(n^2)
# 不稳定:相等元素可能位于不同子序列中。
def shell_sort(lst):
    n = len(lst)
    gap = n // 2
    # while gap >= 1:
    while gap > 0:
        # 进行插入排序,外层走过了所有子序列的轮数。
        # i = [gap,gap+1,gap+2,gap+3,……,n-1]
        # 当i = k*gap(k=0,1,2……) 时是在对子序列0进行操作;
        # 当i = 1 + k*gap 时,是在对子序列1进行操作;
        # 依次类推
        for i in range(gap, n):
            for j in range(i, 0, -gap):
                if lst[j] < lst[j - gap]:
                    lst[j], lst[j - gap] = lst[j - gap], lst[j]
                else:
                    break
        gap = gap // 2


# 答案解法:
def shell_sort2(lst):
    n = len(lst)
    gap = n // 2
    while gap > 0:
        for i in range(gap, n):
            j = i
            # 因为j按gap递减,所以j > 0 (j >= gap )即可。
            while j > 0 and lst[j] < lst[j - gap]:
                lst[j], lst[j - gap] = lst[j - gap], lst[j]
                j -= gap
                #没有做最优化处理。
        gap = gap // 2

快速排序

“分治法”排序。
“最快排序。”
对大数据集效率很高,但是递归的最后几层,效率不高。

思路

选出一个基准。
每次递归都把比基准小的放左边,比基准大的放右边
当分区为空或1时,递归结束。
平均时间复杂度为O(nlogn),比最坏时间复杂度O(n2)有代表性。

实现

#快速排序
# 最优:O(nlogn)
# 平均:O(nlogn)
# 最坏:O(n^2)
# 不稳定:比如当基准取中间值,基准左右都有等值,快排会把所有等值都放在基准的一侧。
#把小于放左侧,大于等于放右侧(若以首元素作为基准,这样稳定)。
def sub_quick_sort(lst,start,end):
    #使用if len(lst) < 2:会出错。
    #空或长度为1,start==end不能包含所有情况。
    if start >= end:
        return
    low = start
    high = end
    #将首元素缓存起来,low的位置就空出来了。
    mid_value = lst[start]
    while low != high:
        #找到第一个需要换位置的
        #因为循环中high始终在移动,所以还需要low!=high保证不越界。
        while low != high and lst[high] >= mid_value:
            high -= 1
        #交换后high位置空出
        lst[low] = lst[high]

        #找到需要换位置的low
        while low != high and lst[low] < mid_value:
            low += 1
        #交换后low位置又空出
        lst[high] = lst[low]

    #mid_index = high也可
    mid_index = low
    lst[mid_index] = mid_value

    sub_quick_sort(lst,start,mid_index-1)
    sub_quick_sort(lst,mid_index+1,end)

#这是一个思想偏向C风格的排序,直接在列表上进行原位修改来排序
#选取首元素作为基准(pivot)
def quick_sort(lst):
    start = 0
    end = len(lst)-1
    sub_quick_sort(lst,start,end)

#一个python风格的快速排序:
#没有原位修改lst,而是返回了一个排好序的列表。
#会额外使用更多空间。
#为便于理解选取中间元素作为基准
def quick_sort_pythonic(lst):
    if len(lst) < 2:
        return lst
    mid_index = len(lst)//2
    mid_value = lst[mid_index]

    left,right = [],[]
    lst.pop(mid_index)

    for x in lst:
        if x < mid_value:
            left.append(x)
        elif x >= mid_value:
            right.append(x)
        else:
            pass
    return quick_sort_pythonic(left) + [mid_value] + quick_sort_pythonic(right)

归并排序

也是分治思想的排序。比快排稍慢,但是稳定。

思路

先递归不断二分,直到子序列长度为1,再逐级合并。

递归深度为logn,合并算法开销为n。

合并算法很容易:
两个指针指向两个待合并序列,每次把小的插入temp,每插入一次,相应指针就后移一位。
注意还要考虑序列长度不等的情况。

实现

# 归并排序
# 最优:O(nlogn)
# 平均:O(nlogn)
# 最坏:O(nlogn)
# 稳定
# 空间:O(n)

def merge_sort(lst):
    left = 0
    right = len(lst)-1
    sub_merge_sort(lst,left,right)

def sub_merge_sort(lst, left, right):
    #其实对于算法本身而言if left==right:就够了
    # > 是为了排除空链表。
    if left >= right:
        return
    mid = (left+right)//2
    sub_merge_sort(lst,left,mid)
    sub_merge_sort(lst,mid+1,right)
    merge(lst,left,mid,right)

#并不需要真的都分成一个一个小序列,只需要把起始和终点的下标记住即可。
def merge(lst, left, mid, right):
    """
    对列表[left:mid+1],[mid+1:right+1]进行合并。
    下标分别是left~mid和mid+1~right。
    """
    p_left = left
    p_right = mid + 1
    temp = []
    # 循环结束后p_left要么大于mid,要么指向下一个需要被插入的元素
    while p_left <= mid and p_right <= right:
        if lst[p_right] < lst[p_left]:
            temp.append(lst[p_right])
            p_right += 1
        else:
            temp.append(lst[p_left])
            p_left += 1

    #如果left或right序列中有没走完的情况:
    while p_left <=mid:
        temp.append(lst[p_left])
        p_left += 1
    while p_right <= right:
        temp.append(lst[p_right])
        p_right += 1

    #列表的片段赋值。区间左闭右开。
    lst[left:right+1] = temp

桶排序

思路

根据桶本身的有序性,将待排序数字依次放入桶中,再挨个将桶中的数倒出来。

假设有n个数字,有m个桶,如果数字是平均分布的,则每个桶里面平均有n/m个数字。
比如有一万个落在在区间[0,1000]内的待排数字,若采用100个桶(大小在[0,10]的放第一个桶,……),那么每个桶里就有100个待排数字。
如果对每个桶中的数字采用快速排序,那么整个算法的复杂度是:
O(n + m * n/m*log(n/m)) = O(n + nlogn – nlogm)

桶越多,速度越快。桶会占据额外空间。

实现

#一个简单的桶排序:
#只能对正整数序列进行排序,而且最大值是多少,就需要多少个桶(+1)。
#时间:O(n+m),m是桶的长度
#空间:O(n+m)
def barrel_sort(lst):
    barrel = [0]*(max(lst)+1)
    for x in lst:
        barrel[x] += 1

    for idx,x in enumerate(barrel):
        for i in range(x):
            print(idx,end= ' ')
    print()

搜索

二分查找

“折半查找”。
适用于有序的顺序表。

思路

不断二分。最终mid指针指向要找的元素。
与归并排序的分割方式一模一样。

实现

#递归实现:
#若找到返回下标。但是若有多个相等元素,只能返回其中一个的下标,而且不能保证是第一个。
#emmm或许返回布尔型更科学???
#递归别忘了写return!
def dichotomy_search(lst, x):
    left = 0
    right = len(lst) - 1
    return dichotomy(lst,left,right,x)

def dichotomy(lst, left, right, x):
    mid = (left + right) // 2
    if left > right:
        return "Not in!"
    if x == lst[mid]:
        return mid
    elif x < lst[mid]:
        return dichotomy(lst, left, mid,x)
    else:
        return dichotomy(lst, mid + 1, right,x)


#循环实现:
def diseciton_search(lst,x):
    left = 0
    right = len(lst)-1
    mid = (left+right)//2

    #注意若把条件写成left<=right,当x取值在序列范围之外时,会陷入死循环。
    while x != lst[mid] and left < right:
        if x < lst[mid]:
            right = mid
            mid = (left+right)//2
        else:
            left = mid+1
            mid = (left+right)//2

    if x == lst[mid]:
        return mid
    else:
        return 'Not in!'
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值