欢迎来到Cecilia陈的博客

孤独,是人一生最好的修行。

常见算法

一、为什么要学习算法与数据结构

  • 算法说直白点算是检验一个人的脑回路了,一个人脑子调理请不清晰,看算法学的好不好,就可以看出个90%
  • 个人觉得算法十比较有趣的,并且很多人都在学

二、算法概述

  • 定义:一个计算过程,解决问题的方法,而程序=数据结构+算法。

  • 时间复杂度:⽤来评估算法运⾏效率的⼀个式⼦。

    print('hello Cecilia')  # 时间复杂度为O(1)
    
    for i in range(n):
        print('hello Cecilia')  # 时间复杂度为O(n)
    
    for i in range(n):
        print('hello Cecilia')
        for i in range(n):
            print('hello Cecilia')  # 时间复杂度为O(n^2)
    
    for i in range(n):
        print('hello Cecilia')
        for i in range(n):
            print('hello Cecilia')
            for i in range(n):
                print('hello Cecilia')  # 时间复杂度为O(n^3)
    
    def func(n):
        while n > 1:
            print(n)
            n = n // 2
    func(64)  #当算法过程出现循环折半时,时间复杂度记为O(logn)
    # 64
    # 32
    # 16
    # 8
    # 4
    # 2
    
  • 如何简单快速的判断时间复杂度(适用于大多数情况)?

    1. 确定问题规模n
    2. 循环减半过程——logn
    3. k层关于n的循环——n^k
  • 空间复杂度:⽤来评估算法内存占⽤⼤⼩的式⼦。空间换时间,表达式跟时间复杂度一样,如下:

    1. 算法使⽤了⼏个变量:O(1)
    2. 算法使⽤了⻓度为n的⼀维列表:O(n)
    3. 算法使⽤了m⾏n列的⼆维列表:O(mn)

三、常见排序算法

3.1 直接插入排序(insert sort)

定义:我们这里以打不可派举例把、当我们在打牌之前每个人都要取摸牌,二没摸一次牌,我们则对拿当前摸得牌和手里已经有的拍进行依次对比,然后找到它对应的位置以后,把他直接放进去,这里需要注意的是,我们的牌默认是从小到大排序的,所以就有了我们直接插入排序的概念:假使一个列表,列表本来就一个元素,则后面再往列表中添加元素的时候,新添加的元素一定要和前面的比较,那这里我们直接插入排序待排序的列表有n个元素,那我们就拿列表的每一个元素和前面的一个元素进行比较,并且再比较的时候,我们的当前元素之前的每个元素默认是拍好顺序的,所以只有当前的元素比它前一个元素小的时候,让这两个元素交换位置,并且继续往前比较,不大于则进行下一个元素的比较

# 这里实现是用python来实现的
def insert_sort(List):
    第一层循环,遍历第二个元素后面的每一个元素
    for x in range(1,len(L)):
        
        第二层循环,选择的每一个元素和他前面的每一个元素比较大小
        for i in range(x-1,-1,-1):
            if L[i] > L[i+1]:
                temp = L[i+1] # 这里使用的是中间变量来进行两个值的交换
                L[i+1] = L[i]
                L[i] = temp

    return List

l=[9,2,45,6,4,23,7,8,6,74,3,35,456,44]
res = insert_sort(l)
print(res)

#时间复杂度为o(n^2)

3.2 希尔排序(shell sort)

定义:分组插⼊排序算法。

思想:

  1. ⾸先取⼀个整数gap=(待排序列表的长度)/2,将元素分为gap个组,每组相邻两元素之间距离为gap,在各组内进⾏直接插⼊排序;
  2. 取第⼆个整数gap=gap/2,重复上述分组排序过程,直到gap=1,即所有元素在同⼀组内进⾏直接插⼊排序。
  3. 希尔排序每趟并不使某些元素有序,⽽是使整体数据越来越接近有序;最后⼀趟排序使得所有数据有序。
def shel_insert(L):
    gap = (int)(len(L)/2) # 首先得到这个定值gap
    while (gap >= 1): # gap必须是要>=1
        
        # 从gap值开始,循环列表的gap位置后的元素
        for x in range(gap,len(L)):
            
            # 从列表的gap位置-gap值开始取值
            # 每次取的到-1前的0位置结束,步长为-gap就实现倒着取
            for i in range(x-gap,-1,-gap):
				# 判断当前取到的值和它组内的一个元素比较大小,然后再判断是否交换位置
                if L[i] > L[i+gap]:
                    temp = L[i+gap]
                    L[i+gap] = L[i]
                    L[i] =temp
                print(L)
                
        gap = (int)(gap/2)# 每次最外层循环结束以后,gap的值//2

shel_insert([9,2,45,6,4,23,7,8,6,74,3,35,456,44])

# 希尔排序的时间复杂度讨论⽐较复杂,并且和选取的gap序列以及列表的长度有关。

3.3 简单选择排序(select sort)

基本思想:比较+交换

解决流程:

  1. 从待排序序列中,取第一个元素作为序列的最小元素,就是序列开始比较的当前位置
  2. 拿这个最小元素和序列当前位置后面所有元素依次进行比较,如果遇到比当前最小元素小的,则更改最小元素的值
  3. 直到与序列后的每一个元素比较完以后,确定找到一个最小的值,然后放在序列的当前位置

总结:简单选择排序也是通过两层循环

  1. 第一层:依次遍历依次遍历序列当中的每一个元素

  2. 第二层循环:将遍历得到的当前元素依次与余下的元素进行比较,符合最小元素的条件,则交换。

l = [9,2,45,6,4,23,7,8,6,74,3,35,456,44]

print(l)

# 依次遍历列表中的每一个元素
for i in range(len(l)):

    # 以当前的位置的元素作为最小的一个元素
    res = l[i]

    # 循环当前作为最小元素后的每一个元素,然后与之做比较
    for j in range(i+1,len(l)):

        # 若当前元素小于我最开始设置的最小元素,那么就将最开始设置的最小元素和当前与它比较的元素交换
        if l[j] < res:
            temp = l[j]
            l[j] = res
            res = temp
        print(l)
    # 直到最小元素与后面的每一个元素比较结束以后,将这个最小值放在排序列表的当前位置[0,1,2,3,4,......]
    l[i]=res
    print(l)

3.4 冒泡排序(bubble sort)

冒泡排序的思想

  1. 保证排序序列中所有的左右元素,有右边的元素总是大于左边的元素的
  2. 在第一次循环结束以后,排好的序列中,最后一个一定是所有元素最大的一个元素
  3. 对于剩下的n-1次循环,一样是重复之前的操作

总结: 冒泡排序就是将序列中的每一个元素和后面的一个元素进行排序,左右一定是右边的比左边的大

  1. 一样需要两层循环
  2. 第一层:循环序列长度-1次
  3. 第二层:从序列的第一个元素开始每一次从当前元素和序列长度的-当前循环的几次的之前的每一个元素进行比较
l = [9,2,45,6,4,23,7,8,6,74,3,35,456,44]

lens = len(l)
for i in range(1,lens):

    for j in range(0,lens-i): # 每一次排序都找到一个最大值,在序列的最后一个位置上
        if l[j] > l[j+1]:
            l[j],l[j+1]= l[j+1],l[j]
    print(l)

print(l)

3.5 快速排序

快速排序的思想:

  • 取⼀个元素p(第⼀个元素),使元素p归位;
  • 列表被p分成两部分,左边都⽐p⼩,右边都⽐p⼤;
  • 递归完成排序。

快速排序的时间复杂度:O(nlogn),完全倒序时间复杂度时:O(n^2)

特殊情况:达到最大递归深度,但是可以修改。

p如何实现归位,以上图为例:先将5拿出来,左指针left和右指针right分别放于两端,假设按顺序升序排序,当右指针移到2时比5小移到5原来的位置,再从左边开始,发现7比5大,将7移到原来2的位置,再从右边开始发现1比5小,将1移动到原来7的位置,再从左边开始移动,4比5小不移动,发现6比5大,所以将6移动到原来1的位置,再从右边开始,发现3比5小,将3移动到原来6的位置,此时3和6之间存在一个空位置,就将5放到该位置,实现了p归位。

def quick_sort(List, left, right):  #列表、左指针位置、右指针位置
    if left < right:
        mid = partition(List, left, right)  # 归位位置
        quick_sort(List, left, mid - 1)  # 递归原函数
        quick_sort(List, mid + 1, right)
        
        
        
def partition(li, left, right):
    """求归位元素函数"""
    tmp = li[left]  # 初始值:归位元素
    while left < right:
        while left < right and li[right] >= tmp: right -= 1  # 从右面找比tmp小的元素,若右面元素大于等于tmp往左移动一位且如果碰上跳出本次循环
        li[left] = li[right]  # 把右边的值赋值给左边空位上
        while left < right and li[left] <= tmp: left += 1  # 从左面找比tmp大的元素,若左面元素小于等于tmp往右移动一位且如果碰上跳出本次循环
        li[right] = li[left]  # 把左边的值赋值给右边空位上
    li[left] = tmp
    return left
def quick_sort(li, left, right):
    """快速排序函数"""
    if left < right:
        mid = partition(li, left, right)
        quick_sort(li, left, mid - 1)
        quick_sort(li, mid + 1, right)
li = [5, 7, 4, 6, 3, 1, 2, 9, 8]
quick_sort(li, 0, len(li) - 1)
print(li)




# 装饰器不能直接加在递归函数上,不然装饰器跟着递归:
import time
import random
def cal_time(func):
    def wrapper(*args, **kwargs):
        t1 = time.time()
        res = func(*args, **kwargs)
        t2 = time.time()
        print(f'{func.__name__} running time:{t2 - t1} secs')
        return res
    return wrapper
def partition(li, left, right):
    """求归位元素函数"""
    tmp = li[left]  # 初始值:归位元素
    while left < right:
        
         # 从右面找比tmp小的元素,若右面元素大于等于tmp往左移动一位且如果碰上跳出本次循环
        while left < right and li[right] >= tmp: right -= 1 
        li[left] = li[right]  # 把右边的值赋值给左边空位上
        
        
         # 从左面找比tmp大的元素,若左面元素小于等于tmp往右移动一位且如果碰上跳出本次循环
        while left < right and li[left] <= tmp: left += 1 
        li[right] = li[left]  # 把左边的值赋值给右边空位上
    li[left] = tmp
    return left
def _quick_sort(li, left, right):  # 换马甲
    if left < right:
        mid = partition(li, left, right)
        _quick_sort(li, left, mid - 1)
        _quick_sort(li, mid + 1, right)
        
@cal_time
def quick_sort(li, left, right):
    _quick_sort(li, left, right)
li = list(range(10000))
random.shuffle(li)
quick_sort(li, 0, len(li) - 1)

3.6 归并排序(merge sort)

一次归并操作:假设现在的列表分两段有序,如何将其合成为⼀个有序列表,如[2,5,7,8,9,1,3,4,6]

归并解题思路:

  • 分解:将列表越分越⼩,直⾄分成⼀个元素。
  • 终⽌条件:⼀个元素是有序的。
  • 合并:将两个有序列表归并,列表越来越⼤。
import random
def merge(li, low, mid, high):
    """
    一次归并函数
    li: 列表
    low: 开始位置索引
    mid: 中间位置索引
    high: 末位置索引
    return:
    """
    i = low
    j = mid + 1
    tmp = []
    while i <= mid and j <= high:  # 保证两边都有值,循环完肯定有一边没数了。
        if li[i] < li[j]:
            tmp.append(li[i])
            i += 1
        else:
            tmp.append(li[j])
            j += 1
    while i <= mid:  # 假设左边有数,循环左边
        tmp.append(li[i])
        i += 1
    while j <= high:  # 假设右边有数,循环右边
        tmp.append(li[j])
        j += 1
    li[low:high + 1] = tmp  # 重新赋值
def merge_sort(li, low, high):
    """归并排序"""
    if low < high:  # 保证至少有两个元素才能递归
        mid = (low + high) // 2
        merge_sort(li, low, mid)
        merge_sort(li, mid + 1, high)
        merge(li, low, mid, high)
li = list(range(20))
random.shuffle(li)
merge_sort(li, 0, len(li) - 1)
print(li)
# 时间复杂度:O(nlogn);空间复杂度:O(n)

3.7 排序总结

快速排序、堆排序、归并排序算法的时间复杂度都是O(nlogn) 
⼀般情况下,就运⾏时间⽽⾔: 快速排序 < 归并排序 < 堆排序 
三种排序算法的缺点: 
    快速排序:极端情况下排序效率低 
    归并排序:需要额外的内存开销 
    堆排序:在快的排序算法中相对较慢

四、计数排序(count sort)

定义:对列表进⾏排序,已知列表中的数范围都在0到100之间。设计时间复杂度为O(n)的算法。

def count_sort(li, max_count=100):
    count = [0 for _ in range(max_count + 1)]
    for val in li:
        count[val] += 1
    li.clear()
    for ind, val in enumerate(count):
        for i in range(val):
            li.append(ind)
import random
li = [random.randint(0, 100) for _ in range(1000)]
count_sort(li)
print(li)

五、桶排序(bucket sort)

定义:⾸先将元素分在不同的桶中,在对每个桶中的元素排序。在计数排序中,如果元素的范围⽐较⼤(⽐如在1到1亿之间), 如何改造算法?

def bucket_sort(li, n=100, max_num=10000):
    buckets = [[] for _ in range(n)]  # 创建桶
    for var in li:
        i = min(var // (max_num // n), n - 1)  # i表示var放到几号桶里
        buckets[i].append(var)
        for j in range(len(buckets[i]) - 1, 0, -1):
            if buckets[i][j] < buckets[i][j - 1]:
                buckets[i][j], buckets[i][j - 1] = buckets[i][j - 1], buckets[i][j]
            else:
                break
    sorted_li = []
    for buc in buckets:
        sorted_li.extend(buc)
    return sorted_li
import random
li = [random.randint(0, 10000) for i in range(100000)]
li = bucket_sort(li)
print(li)
#桶排序的表现取决于数据的分布。也就是需要对不同数据排序时采取不同的分桶策略。 
#平均情况时间复杂度:O(n+k) 
#最坏情况时间复杂度:O(n2k) 
#空间复杂度:O(nk)

六、基数排序

多关键字排序:加⼊现在有⼀个员 ⼯表,要求按照薪资排序,年龄相同的员⼯按照年龄排序。 先按照年龄进⾏排序,再按照薪资进⾏稳定的排序。 对32,13,94,52,17,54,93排序,是否可以看做多关键字排序?

def list_to_buckets(li, base, iteration):
    buckets = [[] for _ in range(base)]
    for number in li:
        digit = (number // (base ** iteration)) % base
    buckets[digit].append(number)
    return buckets
def buckets_to_list(buckets):
    return [x for bucket in buckets for x in bucket]
def radix_sort(li, base=10):
    maxval = max(li)
    it = 0
    while base ** it <= maxval:
        li = buckets_to_list(list_to_buckets(li, base, it))
    it += 1
    return li
# 时间复杂度:O(kn) 
# 空间复杂度:O(k+n) 
# k表示数字位数
posted @ 2019-10-17 00:14  Cecilia陈  阅读(180)  评论(0编辑  收藏  举报