算法

 算法

 时间复杂度

  时间复杂度是用来估算算法运行时间的一个式子(单位)。

  一般来说,时间复杂度高的算法比复杂度低的算法慢。

  常见的时间复杂度(安效率排序):

    O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n2logn) < O(n3)

  不常见的时间复杂度:

    O(n!) < O(2n) < O(nn)

print('Hello World')  # 时间复杂度O(1)

  O(1)  O大约  1表示一个单位

for i in range(n):
    print('Hello World')
# 时间复杂度O(n)

  

for i in range(n):
    for j in range(n):
        print('Hello World')
# 时间复杂度O(n2)

 

for i in range(n):
    for j in range(n):
        for k in range(n):
            print('Hello World')
# 时间复杂度O(n3)

 

print('Hello World')
print('Hello Python')
print('Hello Algorithm')
# 时间复杂度O(1)

 

for i in range(n):
    print('Hello World')
    for j in range(n):
        print('Hello World')
# 时间复杂度O(n2)  根据打印的次数进行判断:n2 + n

 

for i in range(n):
    for j in range(i):
        print('Hello World')
# 时间复杂度O(n2)  根据打印的次数进行判断:0 + 1 + 2 + ... + (n-1)

 

  当两个代码的时间复杂度分别为O(n)与O(n2),谁的运行速度快?

    不一定。当n比较小的时候,不一定谁比较快。一般情况下,我们研究的是n足够大的时候,n足够大,O(n)运行速度快。

  当两段代码的时间复杂度都为O(n2)时,谁的运行速度快?

    不一定。因为其中还牵扯到一个常数项,但是我们计算时间复杂度时,都将常数项忽略了。

  当出现循环折半的时候,代码的时间复杂度就为O(logn).

while n > 1:
    print(n)
    n = n // 2
# 时间复杂度O(logn)

  如何判断时间复杂度?

    循环减半的过程:O(logn);

    几次循环就是n的几次方的复杂度。

空间复杂度

    空间复杂度:用来评估算法内存占用大小的一个式子。

  使用一个变量。空间复杂度O(1)

  使用了一个长度为n的列表。空间复杂度O(n)

  使用了n个长度为n的列表。空间复杂度O(n2)

  空间换时间

  一般不考虑空间复杂度,除非代码特别复杂,会吃爆内存或者有要求时考虑。

递归

  递归的两个特点:

    调用自身

    结束条件

  假设x为3:

def func(x):
    if x > 0:
        print(x)
        func(x-1)


func(3)
''' 输出结果: 3 2 1 '''

  递归调用图解:

 

def func(x):
    if x > 0:
        func(x-1)
        print(x)


func(3)
''' 输出结果: 1 2 3 '''

 递归调用图解:

 斐波那契

  求斐波那契第n项

  1 1 2 3 5 8 13

  # 假设小规模的问题能解决的条件下,能设计步骤解决原问题

  # 当n比较大时就比较慢,重复计算子问题导致的。

  递归写法:

def fib(n):
'''
递归写法
'''
    if n == 0 or n == 1:
        return 1
    else:
        return fib(n-1) + fib(n-2)

print(fib(10))

   依次调用原函数,给传入的数一次减一,直至递归找到第0项或者第1项。时间复杂度O(2n)

   不重复计算子问题写法:

def fib(n):
'''
不重复计算子问题
'''
    res = [1, 1]
    for i in range(2, n+1):
        res.append(res[-1] + res[-2])
     return res[-1]

print(fib(100))

   将第0项和第1项放入列表,然后一次循环,每次取列表的后两项求和存入列表,循环n次,最后取最后一项即为第n个数。时间复杂度O(n) 空间复杂度O(n)

  没有空间复杂度写法:

def fib(n):
    if n == 0 or n == 1:
        return 1
    a = 1
    b = 1
    c = 2  # 前两项之和
    for i in range(n, n+1):
        c = a + b
        a = b
        b = c
    return c

print(fib(100))

   就是重复赋值。

汉诺塔问题

def hanoi(n, A, B, C):
    if n > 0:
        hanoi(n-1, A, C, B)
        print(f'{A}->{C}')
        hanoi(n-1, B, A, C)

hanoi(3, 'A', 'B', 'C')

 习题

  一段有n个台阶组成的楼梯,小明从楼梯的底层向最高层前进,他可以选择一次迈一级台阶或者一次迈两级台阶。问:他有多少种不同的走法?

列表查找

列表查找

  从列表中查找指定元素。

  输入:列表、待查找元素

  输出:元素下标或未查找到元素

顺序查找

  从列表第一个元素开始,顺序进行搜索,直到找到为止。

二分查找

  从有序列表的候选区data[0:n]开始,通过对待查找的值与候选区中间值的比较,可以使候选区减少一半。

  非递归写法:

def bin_search(li, val):
    low = 0
    high = len(li) - 1
    while low <= high:  # 候选区有值
        mid = (low + high) // 2
        if li[mid] == val:
            return mid
        elif li[mid] > val:
            high = mid - 1
        else:
            low = mid + 1
    return -1

bin_search(li, val)

   递归写法:(不推荐)

def bin_search_rec(data_set, value, low, high):
    if low <= high:
        mid = (low + high) // 2
        if data_set[mid] == value:
            return mid
        elif data_set[mid] > value:
            return bin_search_rec(data_set, value, low, mid-1)
        else:
            return bin_search_rec(data_set, value, mid+1, high)
    else:
        return - 1

def bin_search_rec(data_set, value, low, high)

 排序Low B三人组

冒泡排序

  首先,列表每两个相邻的数,如果前边的比后边的大,那么交换着两个数......

def bubble_sort(li):
    for i in range(len(li)-i):  # i表示第i趟
        for j in range(len(li)-i-1):
            if li[j] > li[j+1]:
                li[j], li[j+1] = li[j+1], li[j]

bubble_sort(li)

  优化版:

def bubble_sort(li):
    for i in range(len(li)-i):
        exchange = False
        for j in range(len(li)-i-1):
            if li[j] > li[j+1]:
                li[j], li[j+1] = li[j+1], li[j]
                exchange = True
        if not exchange:
             break

bubble_sort(li)

时间复杂度

  O(n2)

空间复杂度

  O(1)

选择排序

   一趟遍历记录最小的数,放到第一个位置;再一趟遍历记录剩余列表(无序区)中最小的数,继续放置到无序区的第一个位置;依次重复,直到结束。

def select_sort(li):
    for i in range(len(li)-1):
        # 无序区的范围 i, n-1
        min_pos = i
        for j in range(i+1, len(li)):
            if li[j] < li[min_pos]:
                min_pos = j
        li[i], li[min_pos] = li[min_pos], li[i]
        
select_sort()

 

   选择排序没有最好的情况,即使是排好序的列表,时间复杂度也是不变。

时间复杂度

  O(n2)

空间复杂度

  O(1)

插入排序

   列表元素分为有序区和无序区两部分。最初有序区只有一个元素。每次从无序区选择一个元素,插入到有序区的位置,直到无序区变空。

def insert_sort(li):
    for i in range(1, len(li)):
        j = i - 1
        tmp = li[i]
        while j >= 0 and li[j] > tmp:  # 这两个条件最好不要写反了,Python中支持负索引,其他语言中不支持,就会直接报错程序挂掉
            li[j+1] = li[j]
            j -= 1
        li[j+1] = tmp

insert_sort()

 

  当列表为有序时,存在最好情况。

时间复杂度

  O(n2)

空间复杂度

  O(1)

 NB三人组

快速排序 

  取一个元素P(第一个元素),使元素P归位;列表被P分成两部分,左边都比P小,右边都比P大;递归完成排序。

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)

quick_sort(li, 0, len(li)-1)

 

方式一

def partition(li, left, right):
    tmp = li[left]
    while left < right:
        while left < right and li[right] >= tmp:
            right -= 1
        li[left] = li[right]
        while left < right and li[left] <= tmp:
            left += 1
        li[right] = li[left]
    li[left] = tmp
    return left

 

方式二

def partition(li, left, right):
    tmp = li[right]
    i = left - 1
    for j in range(left, right):
        if li[j] <= tmp:  # 归到左半部分
            i += 1
            li[i], li[j] = li[j], li[i]
        li[right], li[i+1] = li[i+1], li[right]
        return i+1

 

 优点

  快

时间复杂度

  O(nlogn)

最坏情况

  每次都选到最大或者最小的数,超过最大递归深度。

改进

  针对方式一,如果将列表改成降序,只需要将partition中的 li[right] >= tmp 改成 li[right] <= tmp和 li[left] <= tmp 改成 li[left] >= tmp即可。

堆排序

 树

def sift(li, low, high):
    '''

    :param li:
    :param low: 堆顶
    :param high: 最后一个
    :return:
    '''
    tmp = li[low]
    i = low
    j = 2 * i + 1
    while j <= high:  # 退出循环的第二种情况:i已经是最后一层了
        if j + 1 <= high and li[j+1] > li[j]:  # 右孩子存在并且大于左孩子
            j += 1
        if tmp < li[j]:
            li[i] = li[j]
            i = j
            j = 2 * i + 1
        else:
            break  # 退出循环的第一种情况:j位置比tmp小
    li[i] = tmp


def heap_sort(li):
    '''
    建堆
    :param li:
    :return:
    '''
    n = len(li)
    for low in range(n//2-1, -1, -1):
        sift(li, low, n-1)
        # 挨个输出,退休-棋子-调整 重复n次或者n-1次
    for high in range(n-1, -1, -1):
        li[0], li[high] = li[high], li[0]
        sift(li, 0, high-1)

heap_sort()

  比快排稍微慢点,但是还是挺快的。

时间复杂度

  sift()  O(logn)

  deap_sort()  O(n)

  整体时间复杂度:O(nlogn)

python内置堆排

  python拥有内置的堆排序模块heapq

  优先队列:一些元素的集合,POP操作每次执行都会从优先队列中弹出最大(或最小)的元素。

  堆——优先队列

import heapq

li = [5, 7, 9, 8, 4, 1, 6, 2, 3]

heapq.heapify(li)  # 建堆(小根堆)
print(li)  # [1, 2, 5, 3, 4, 9, 6, 8, 7]  小根堆

heapq.heapify(li, 0)  # 给堆添加元素, 前边是列表,后边是要添加的元素
print(li)  # [0, 1, 2, 5, 3, 4, 9, 6, 8, 7]

num = heapq.heappop(li)  # 最小的元素出来
print(num)  # [0]

# 前n大的数
num1 = heapq.nlagest(5, [1, 2, 5, 4, 7, 8, 9, 6, 3]) # 5为多少个数,列表为待查询列表
print(num1) # [9, 8, 7, 6, 5]

# 前n小的数
num2 = heapq.nsmallgest(5,
[1, 2, 5, 4, 7, 8, 9, 6, 3]) # 5为多少个数,列表为待查询列表
print(num2)  # [1, 2, 3, 4, 5]

使用heapq模块实现堆排序

def heapq_sort(li):
    h = []
    for value in li:
        heapqpush(h, value)
    return [heappop(h) for i in range(len(h))]

heapq_sort()

应用

优先队列
topK问题。(n个数中找出前k大的数)
思路

  取列表前k个元素建立一个小根堆。堆顶就是目前第k大的数。依次向后遍历原列表,对于列表中的元素,如果小于堆顶,则忽略该元素;如果大于堆顶,则将堆顶更换为该元素,并且对堆进行一次调整。遍历列表所有元素后,倒序弹出堆顶。

def topk(li, k):
    heap = li[0: k]

    # 建堆
    for i in range(k // 2 - 1, -1, -1):
        sift(heap, i, k - 1)

    # 插进去调整
    for i in range(k, len(li)):
        if li[i] > heap[0]:
            heap[0] = li[i]
            sift(heap, 0, k - 1)

    # 挨个出数
    for i in range(k - 1, -1, -1):
        heap(0), heap[-1] = heap[i], heap[0]
        sift(heap, 0, i - 1)


def sift(li, low, high):
    '''

    :param li:
    :param low: 堆顶
    :param high: 最后一个
    :return:
    '''
    tmp = li[low]
    i = low
    j = 2 * i + 1
    while j <= high:  # 退出循环的第二种情况:i已经是最后一层了
        if j + 1 <= high and li[j+1] < li[j]:  # 右孩子存在并且小于左孩子
            j += 1
        if tmp > li[j]:
            li[i] = li[j]
            i = j
            j = 2 * i + 1
        else:
            break  # 退出循环的第一种情况:j位置比tmp小
    li[i] = tmp

topk()

 

归并排序

   假设现在的列表分两段有序(例如一半升序,一半降序),将其合成一个有序列表。这种操作称为一次归并。

一次归并

def merge(li, low, mid, high):
    i = low
    j = mid + 1
    li_tmp = []
    while i <= mid and j <= high:
        if li[i] <= li[j]:
            li_tmp.append(li[i])
            i += 1
        else:
            li_tmp.append(li[j])
            j += 1
    while i <= mid: 
        li_tmp.append(li[i])
        i += 1
    while j <= high:
        li_tmp.append(li[i])
        j += 1
for k in range(low, high + 1): li[k] = li_tmp[k - low] # li[low: high + 1] = li_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 = [10, 4, 6, 3, 8, 2, 5, 7]
merge_sort(li, 0, len(li) - 1)
print(li)

 

时间复杂度

  O(nlogn)

空间复杂度

  O(n)

   三种排序算法的时间复杂度都是O(nlogn)。

  一般情况下,就运行时间而言:

    快速排序 < 归并排序 < 堆排序

  三种排序算法的缺点:

    快速排序:

      极端情况下排序效率低

    归并排序:

      需要额外的内存开销

    堆排序:

      在快的排序算法中相对较慢

  前6种算法的简单总结

  python的排序算法是Timsort。使用归并和插入算法混合实现。

没什么人用的排序

计数排序

   一个列表,列表中的元素的范围都在0到100之间。设计算法在O(n)时间复杂度内将列表进行排序。

  思想:

    建一个列表,统计里边的每个元素的个数,统计每个数出现的次数,再根据统计的结果一次输出排序的结果。

def count_sort(li, max_num=100):
    count = [0 for i in range(max_num+1)]
    for val in li:
        count[val] += 1
    li.clear()
    for i in range(len(count)):
        for _ in range(count[i]):
            li.append(i)

count_sort()

 

时间复杂度

  O(n)

希尔排序

   希尔排序是一种分组插入排序算法。首先取一个整数d1=n/2,将元素分为d1个组,魅族相邻两元素之间距离为d1,在各组内进行直接插入排序;去第二个整数d2=d1/2,重复上述分组排序过程,知道di=1,即所有元素在同一组内进行直接插入排序。

  希尔排序每趟并不是某些元素有序,而是使整体数据越来越接近有序;最后一趟使得所有的数据有序。

def shell_insert_sort(li, d):
    for i in range(d, len(li)):
        j = i - d
        tmp = li[i]
        while j >= 0 and li[j] > tmp:
            li[j + d] = li[j]
            j -= d
        li[j + d] = tmp

def shell_sort(li):
    d = len(li) // 2
    while d > 0:
        shell_insert_sort(li, d)
        d = d // 2

shell_sort()

桶排序

   在技术排序中,如果元素范围比较大(比如在1到1亿之间),name这个算法就不在适用。这时可以使用桶排序。

  桶排序:

    首先将元素分在按不同的桶中,在对每个桶(列表)中的每个元素进行排序。

  桶排序的表现取决于数据的分布。也就是需要对不同数据排序时采取不同的分桶策略。

时间复杂度

  平均时间复杂度:O(n+k)

  最坏情况时间复杂度:O(n2k)

空间复杂度

  O(nk)

基数排序

  多关键字排序:

    假如现在一个员工表,要求按照薪资排序,年龄形同的员工按照年龄排序。先按照年龄进行排序,再按照薪资进行稳定的排序。

def list_to_buckets(li, i):
    buckets = [[] for _ in range(10)]
    for num in li:
        digit = num // (10 ** i) % 10
        buckets[digit].append(num)
    return buckets


def radix_sort(li):
    max_val = max(li)
    i = 0
    while 10 ** i <= max_val:
        li = list_to_buckets(li, i)
        i += 1
    return li

radix_sort()

 

时间复杂度

  O(nk)

    k表示数字位数

空间复杂度

  O(k + n)

    k表示数字位数

 

posted @ 2019-02-05 18:43  AKA绒滑服贵  阅读(239)  评论(0编辑  收藏  举报