Algorithm

算法基礎

  • 算法(Algorithm):一個計算過程,解決問題的方法。
  • N.Wirth: 「程序=數據結構+算法」

1、時間複雜度

  • 時間複雜度是用來估計算法運行時間的一個式子(單位)
  • 通常來講,時間複雜度高的算法比複雜度低的算法慢。
  • 常見的時間複雜度(按效率排序) O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n2logn) < O(n3)
  • 複雜問題的時間複雜度O(n!) O(2n) O(nn) …

一、如何簡單快速地判斷算法複雜度

  • 快速判斷算法複雜度(適用於絕大多數簡單狀況):
    1. 肯定問題規模n
    2. k層關於n的循環→nk
    3. 循環減半過程→logn
  • 複雜狀況:根據算法執行過程判斷

2、空間複雜度

  • 空間複雜度:用來評估算法內存佔用大小的式子
  • 空間複雜度的表示方式與時間複雜度徹底同樣
  • 算法使用了幾個變量:O(1)
  • 算法使用了長度爲n的一維列表:O(n)
  • 算法使用了m行n列的二維列表:O(mn)
  • 「空間換時間」
def hanoi(n, a, b, c):
    if n>0:
        hanoi(n-1, a, c, b)
        print("moving from %s to %s" % (a, c))
        hanoi(n-1, b, a, c)

hanoi(3, 'A', 'B', 'C')
"""
moving from A to C
moving from A to B
moving from C to B
moving from A to C
moving from B to A
moving from B to C
moving from A to C
"""
遞歸實例:漢諾塔問題

列表查找

  • 查找:在一些數據元素中,經過必定的方法找出與給定關鍵字相同的數據元素的過程。
  • 列表查找(線性表查找):從列表中查找指定元素
  • 輸入:列表、待查找元素
  • 輸出:元素下標(未找到元素時通常返回None或-1)
  • 內置列表查找函數:index()

1、順序查找 (Linear Search)

  • 順序查找:也叫線性查找,從列表第1個元素開始,順序進 行搜索,直到找到元素或搜索到列表最後1個元素爲止。
  • 時間複雜度:O(n)

2、二分查找 (Binary Searh)

  • 二分查找:又叫折半查找,從有序列表的初始候選區li[0:n]開 始,經過對待查找的值與候選區中間值的比較,可使候選 區減小一半。
  • 時間複雜度:O(logn)
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
遞歸版本的二分查找

列表排序

  • 排序:將一組「無序」的記錄序列調整爲「有序」的記錄序列。
  • 列表排序:將無序列表變爲有序列表
  • 輸入:列表
  • 輸出:有序列表
  • 升序與降序
  • 內置排序函數:sort()

1、排序Low B三人組

一、冒泡排序 (Bubble Sort)

  • 列表每兩個相鄰的數,若是前面比後面大,則交換這兩個數。
  • 一趟排序完成後,則無序區減小一個數,有序區增長一個數。
  • 代碼關鍵點:趟、無序區範圍
  • 若是冒泡排序中的一趟排序沒有發生交換,則說明列表已經有序,能夠直接結束算法。
  • 時間複雜度:O(n2)
計時裝飾器
import random
from .timewrap import *

@cal_time
def bubble_sort(li):
    for i in range(len(li) - 1):
        # i 表示趟數
        # 第 i 趟時: 無序區:(0,len(li) - i)
        for j in range(0, len(li) - i - 1):
            if li[j] > li[j+1]:
                li[j], li[j+1] = li[j+1], li[j]

#冒泡排序優化
#若是冒泡排序中執行一趟而沒有交換,則列表已是有序狀態,能夠直接結束算法。
@cal_time
def bubble_sort_2(li):
    for i in range(len(li) - 1):
        # i 表示趟數
        # 第 i 趟時: 無序區:(0,len(li) - i)
        change = False
        for j in range(0, len(li) - i - 1):
            if li[j] > li[j+1]:
                li[j], li[j+1] = li[j+1], li[j]
                change = True
        if not change:
            return

li = list(range(10000))
# random.shuffle(li)
# print(li)
bubble_sort_2(li)
print(li)

'''
列表每兩個相鄰的數,若是前邊的比後邊的大,那麼交換這兩個數
時間複雜度O(n2)
'''
View Code

二、選擇排序 (Select Sort)

  • 一趟排序記錄最小的數,放到第一個位置
  • 再一趟排序記錄記錄列表無序區最小的數,放到第二個位置
  • ……
  • 算法關鍵點:有序區和無序區、無序區最小數的位置
  • 時間複雜度:O(n2)
import random
from .timewrap import *

@cal_time
def select_sort(li):
    for i in range(len(li) - 1):
        # i 表示趟數,也表示無序區開始的位置
        min_loc = i   # 最小數的位置
        for j in range(i + 1, len(li) - 1):
            if li[j] < li[min_loc]:
                min_loc = j
        li[i], li[min_loc] = li[min_loc], li[i]


li = list(range(10000))
random.shuffle(li)
print(li)
select_sort(li)
print(li)


'''
一趟遍歷記錄最小的數,放到第一個位置;
再一趟遍歷記錄剩餘列表中最小的數,繼續放置;
時間複雜度:O(n2)
'''
View Code

三、插入排序

  • 初始時手裏(有序區)只有一張牌
  • 每次(從無序區)摸一張牌,插入到手裏已有牌的正確位置
  • 時間複雜度:O(n2)
import random
from .timewrap import *

@cal_time
def insert_sort(li):
    for i in range(1, len(li)):
        # i 表示無序區第一個數
        tmp = li[i] # 摸到的牌
        j = i - 1 # j 指向有序區最後位置
        while li[j] > tmp and j >= 0:
            #循環終止條件: 1. li[j] <= tmp; 2. j == -1
            li[j+1] = li[j]
            j -= 1
        li[j+1] = tmp


li = list(range(10000))
random.shuffle(li)
print(li)
insert_sort(li)
print(li)

'''
列表被分爲有序區和無序區兩個部分。最初有序區只有一個元素。
每次從無序區選擇一個元素,插入到有序區的位置,直到無序區變空。
O(n2)

'''
View Code

2、排序NB三人組

一、快速排序

  • 快速排序:
  • 快排思路:
    1. 取一個元素p(第一個元素),使元素p歸位;
    2. 列表被p分紅兩部分,左邊都比p小,右邊都比p大;
    3. 遞歸完成排序。
  • 時間複雜度:
    1. 時間複雜度:O(nlogn)
  • 問題:
    1. 最壞狀況
    2. 遞歸
View Code

二、堆排序

  • 堆:一種特殊的徹底二叉樹結構
  • 大根堆:一棵徹底二叉樹,知足任一節點都比其孩子節點大
  • 小根堆:一棵徹底二叉樹,知足任一節點都比其孩子節點小
  • 堆的向下調整性質:
    1. 假設根節點的左右子樹都是堆,但根節點不知足堆的性質
    2. 能夠經過一次向下的調整來將其變成一個堆。
  • 堆排序過程:
    1. 創建堆
    2. 獲得堆頂元素,爲最大元素
    3. 去掉堆頂,將堆最後一個元素放到堆頂,此時可經過一次調整從新使堆有序。
    4. 堆頂元素爲第二大元素。
    5. 重複步驟3,直到堆變空。
  • Python內置模塊:
    1. heapq
  • 經常使用函數:
    1. heapify(x)
    2. heappush(heap, item)
    3. heappop(heap)
  • 優先隊列:
    1. 一些元素的集合,POP操做每次執行都會從優先隊列中彈出最大(或最小)的元素。
  • topk問題:
    1. 如今有n個數,設計算法獲得前k大的數。(k<n)
    2. 解決思路:排序後切片 O(nlogn)
    3. 解決思路:排序LowB三人組 O(mn)
    4. 取列表前k個元素創建一個小根堆。堆頂就是目前第k大的數。
    5. 依次向後遍歷原列表,對於列表中的元素,若是小於堆頂,則忽略該元 素;若是大於堆頂,則將堆頂更換爲該元素,而且對堆進行一次調整;
    6. 遍歷列表全部元素後,倒序彈出堆頂
  • 時間複雜度:O(nlogn)
from .timewrap import *
import random

def _sift(li, low, high):
    """
    :param li:
    :param low: 堆根節點的位置
    :param high: 堆最有一個節點的位置
    :return:
    """
    i = low  # 父親的位置
    j = 2 * i + 1  # 孩子的位置
    tmp = li[low]  # 原省長
    while j <= high:
        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:
            li[i] = tmp  # 省長放到對應的位置上(幹部)
            break
    else:
        li[i] = tmp  # 省長放到對應的位置上(村民/葉子節點)


def sift(li, low, high):
    """
    :param li:
    :param low: 堆根節點的位置
    :param high: 堆最有一個節點的位置
    :return:
    """
    i = low         # 父親的位置
    j = 2 * i + 1   # 孩子的位置
    tmp = li[low]   # 原省長
    while j <= high:
        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
    li[i] = tmp


@cal_time
def heap_sort(li):
    n = len(li)
    # 1. 建堆
    for i in range(n//2-1, -1, -1):
        sift(li, i, n-1)
    # 2. 挨個出數
    for j in range(n-1, -1, -1):    # j表示堆最後一個元素的位置
        li[0], li[j] = li[j], li[0]
        # 堆的大小少了一個元素 (j-1)
        sift(li, 0, j-1)


li = list(range(10000))
random.shuffle(li)
heap_sort(li)
print(li)

# li=[2,9,7,8,5,0,1,6,4,3]
# sift(li, 0, len(li)-1)
# print(li)

''''
創建堆
獲得堆頂元素,爲最大元素
去掉堆頂,將堆最後一個元素放到堆頂,此時可經過一次調整從新使堆有序。
堆頂元素爲第二大元素。
重複步驟3,直到堆變空。

時間複雜度O(nlgn)
'''
View Code
import heapq, random

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

heapq.heapify(li)
print(heapq.heappop(li))
print(heapq.heappop(li))

def heap_sort(li):
    heapq.heapify(li)
    n = len(li)
    new_li = []
    for i in range(n):
        new_li.append(heapq.heappop(li))
    return new_li

li = list(range(10000))
random.shuffle(li)
# li = heap_sort(li)
# print(li)

print(heapq.nlargest(100, li))
View Code

三、歸併排序

  • 假設如今的列表分兩段有序,如何將其合成爲一個有序列表這種操做稱爲一次歸併
  • 分解:將列表越分越小,直至分紅一個元素。
  • 終止條件:一個元素是有序的。
  • 合併:將兩個有序列表歸併,列表愈來愈大
  • 時間複雜度:O(nlogn)
import random
from timewrap import *
import copy
import sys


def merge(li, low, mid, high):
    i = low
    j = mid + 1
    ltmp = []
    while i <= mid and j <= high:
        if li[i] < li[j]:
            ltmp.append(li[i])
            i += 1
        else:
            ltmp.append(li[j])
            j += 1
    while i <= mid:
        ltmp.append(li[i])
        i += 1
    while j <= high:
        ltmp.append(li[j])
        j += 1
    li[low:high+1] = ltmp


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)
        print(li[low:high+1])


def merge_sort(li):
    return _merge_sort(li, 0, len(li)-1)


li = list(range(16))
random.shuffle(li)
print(li)
merge_sort(li)

print(li)
View Code

NB三人組小結

  • 時間複雜度都是O(nlogn)
  • 通常狀況下,就運行時間而言: 快速排序 < 歸併排序 < 堆排序
  • 三種排序算法的缺點:
    1. 快速排序:極端狀況下排序效率低
    2. 歸併排序:須要額外的內存開銷
    3. 堆排序:在快的排序算法中相對較慢

3、其餘排序

一、希爾排序

  • 希爾排序(Shell Sort)是一種分組插入排序算法。
  • 首先取一個整數d1=n/2,將元素分爲d1個組,每組相鄰量元素之間距離爲d1,在各組內進行直接插入排序;
  • 取第二個整數d2=d1/2,重複上述分組排序過程,直到di=1,即全部元素在同一組內進行直接插入排序。
  • 希爾排序每趟並不使某些元素有序,而是使總體數據愈來愈接近有序;最後一趟排序使得全部數據有序。
  • 希爾排序的時間複雜度討論比較複雜,而且和選取的gap序列有關。
def insert_sort(li):
    for i in range(1, len(li)):
        # i 表示無序區第一個數
        tmp = li[i] # 摸到的牌
        j = i - 1 # j 指向有序區最後位置
        while li[j] > tmp and j >= 0:
            #循環終止條件: 1. li[j] <= tmp; 2. j == -1
            li[j+1] = li[j]
            j -= 1
        li[j+1] = tmp

def shell_sort(li):
    d = len(li) // 2
    while d > 0:
        for i in range(d, len(li)):
            tmp = li[i]
            j = i - d
            while li[j] > tmp and j >= 0:
                li[j+d] = li[j]
                j -= d
            li[j+d] = tmp
        d = d >> 1
View Code

二、計數排序

  • 對列表進行排序,已知列表中的數範圍都在0到100之間。設計時間複雜度爲O(n)的算法
# 0 0 1 1 2 4 3 3 1 4 5 5
import random
import copy
from timewrap import *

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

@cal_time
def sys_sort(li):
    li.sort()

li = [random.randint(0,100) for i in range(100000)]
li1 = copy.deepcopy(li)
count_sort(li)
sys_sort(li1)
View Code

三、桶排序

  • 在計數排序中,若是元素的範圍比較大(好比在1到1億之間),如何改造算法?
  • 桶排序(Bucket Sort):首先將元素分在不一樣的桶中,在對每一個桶中的元素排序。
  • 桶排序的表現取決於數據的分佈。也就是須要對不一樣數據排序時採起不一樣的分桶策略。
  • 平均狀況時間複雜度:O(n+k)
  • 最壞狀況時間複雜度:O(n2k)
  • 空間複雜度:O(nk)

四、基數排序

  • 多關鍵字排序:加入如今有一個員工表,要求按照薪資排序,年齡相同的員工按照年齡排序。
  • 先按照年齡進行排序,再按照薪資進行穩定的排序。
  • 時間複雜度:O(kn)
  • 空間複雜度:O(k+n)
  • k表示數字位數
import random
from .timewrap import *

def list_to_buckets(li, iteration):
    """
    :param li: 列表
    :param iteration: 裝桶是第幾回迭代
    :return:
    """
    buckets = [[] for _ in range(10)]
    for num in li:
        digit = (num // (10 ** iteration)) % 10
        buckets[digit].append(num)
    return buckets

def buckets_to_list(buckets):
    return [num for bucket in buckets for num in bucket]
    # li = []
    # for bucket in buckets:
    #     for num in bucket:
    #         li.append(num)

@cal_time
def radix_sort(li):
    maxval = max(li) # 10000
    it = 0
    while 10 ** it <= maxval:
        li = buckets_to_list(list_to_buckets(li, it))
        it += 1
    return li

li = [random.randint(0,1000) for _ in range(100000)]
radix_sort(li)
View Code

給定一個升序列表和一個整數,返回該整數在列表中的下標範圍

def twoSum(nums, target):
    """
    :type nums: List[int]
    :type target: int
    :rtype: List[int]
    """
    for i in range(len(nums)):
        for j in range(i+1,len(nums)):
            if nums[i]+nums[j] == target:
                return [i,j]
View Code
def twoSum(nums, target):

    d = {}
    for i in range(len(nums)):
        if target - nums[i] in d:
                return i, d[target-nums[i]]
        else:
            d[nums[i]] = i
View Code

找出第k大的數

def partition(array, left, right):
    well = left
    for i in range(left, right):
        if array[i] > array[right]:
            array[i], array[well] = array[well], array[i]
            well += 1
    array[well], array[right] = array[right], array[well]
    return well

def findk(l,low,high,k):
    if low<=high:
        mid=partition(l, low, high)
        if mid==len(l)-k:
            res=l[mid]
            return res
        elif mid>len(l)-k:
            return findk(l,low,mid-1,k)
        else:
            return findk(l,mid+1,high,k)



l=[1,23,4,5,64,68,12,45,666,999,69]
res=findk(l,0,len(l)-1,6)
print("res:",res,l)
View Code
def kp(data, left, right):
    tem = data[left]
    while left < right:
        while left < right and data[right] >= tem:
            right -= 1
        data[left] = data[right]
        while left < right and data[left] <= tem:
            left += 1
        data[right] = data[left]

    data[right] = tem
    return left

def qk(data, left, right):
    if left <right:
        mid = kp(data, left,right)
        qk(data,left, mid-1)
        qk(data,mid+1, right)




k=5
data = [1, 23, 4, 5, 64, 68, 12, 45, 666, 999, 69]
qk(data, 0, len(data)-1)
data.reverse()
print(data)
print(data[len(data) -1-k-1])
View Code

貪心算法

  • 貪心算法(又稱貪婪算法)是指,在對問題求解時,老是作 出在當前看來是最好的選擇。也就是說,不從總體最優上加 以考慮,他所作出的是在某種意義上的局部最優解。
  • 貪心算法並不保證會獲得最優解,可是在某些問題上貪心算 法的解就是最優解。要會判斷一個問題可否用貪心算法來計 算。

1、找零問題

  • 假設商店老闆須要找零n元錢,錢幣的面額有:100元、50元、 20元、5元、1元,如何找零使得所需錢幣的數量最少?

2、揹包問題

  • 一個小偷在某個商店發現有n個商品,第i個商品價值vi元,重wi千克。他但願 拿走的價值儘可能高,但他的揹包最多隻能容納W千克的東西。他應該拿走哪些 商品?
  • 0-1揹包:對於一個商品,小偷要麼把它完整拿走,要麼留下。不能只拿走一部分,或把一個商品拿走屢次。(商品爲金條)
  • 分數揹包:對於一個商品,小偷能夠拿走其中任意一部分。(商品爲金砂)

3、拼接最大數字問題

  • 有n個非負整數,將其按照字符串拼接的方式拼接爲一個整數。 如何拼接可使得獲得的整數最大?

4、活動選擇問題

  • 假設有n個活動,這些活動要佔用同一片場地,而場地在某時 刻只能供已個活動使用。
  • 每一個活動都有一個開始時間si和結束時間fi(題目中時間以整數 表示),表示活動在[si, fi)區間佔用場地。
  • 問:安排哪些活動可以使該場地舉辦的活動的個數最多?
  • 貪心結論:最早結束的活動必定是最優解的一部分。
  • 證實:假設a是全部活動中最早結束的活動,b是最優解中最早結束的活動。
  • 若是a=b,結論成立。
  • 若是a≠b,則b的結束時間必定晚於a的結束時間,則此時用a替換掉最優解中 的b,a必定不與最優解中的其餘活動時間重疊,所以替換後的解也是最優解

動態規劃

  • 從斐波那契數列看動態規劃
  • 斐波那契數列:Fn= Fn−1 + Fn−2
  • 練習:使用遞歸和非遞歸的方法來求解斐波那契數列的第n項

1、鋼條切割問題

2、動態規劃問題關鍵特徵

  • 最優子結構:
    1. 原問題的最優解中涉及多少個子問題
    2. 在肯定最優解使用哪些子問題時,須要考慮多少種選擇
  • 重疊子問題

3、最長公共子序列

歐幾里得算法

1、最大公約數

  • 約數:若是整數a能被整數b整除,那麼a叫作b的倍數,b叫作 a的約數。
  • 給定兩個整數a,b,兩個數的全部公共約數中的最大值即爲最 大公約數(Greatest Common Divisor, GCD)。
  • 例:12與16的最大公約數是4

RSA加密算法簡介

  • 傳統密碼:加密算法是祕密的
  • 現代密碼系統:加密算法是公開的,密鑰是祕密的
  • 對稱加密系統
  • 非對稱加密系統:
    1. 公鑰:用來加密,是公開的
    2. 私鑰:用來解密,是私有的

1、RSA加密算法過程

  • 隨機選取兩個質數p和q
  • 計算n=pq
  • 選取一個與φ(n)互質的小奇數e,φ(n)=(p-1)(q-1)
  • 對模φ(n),計算e的乘法逆元d,即知足 (e*d) mod φ(n) = 1
  • 公鑰(e, n) 私鑰(d, n)
  • 加密過程:c = (m^e) mod n
  • 解密過程:m = (c^d) mod n
相關文章
相關標籤/搜索