算法学习笔记(持续更新中...)

第一章

二分查找

二分查找是一种算法, 其输入是一个有序的元素列表. 如果要查找的元素包含在列表中, 二分查找返回其位置: 否则返回 NULL

一般而言, 对于包含N个元素的列表, 用二分查找最多需要log2^N步, 而简单查找最多需要N步.

# –*– coding: utf-8 –*–
# @Time : 2019/1/4 21:53
# @Author : Damon_duan
# @FileName : binary_search.py
# @BlogsAddr : https://blog.csdn.net/Damon_duanlei
import time
import random


def binary_search(find_list, search_info):
    low = 0
    high = len(find_list) - 1
    while low <= high:
        mid = (low + high) // 2
        if search_info == find_list[mid]:
            return mid
        if search_info > find_list[mid]:
            low = mid + 1
        else:
            high = mid - 1
    return None


def easy_search(find_list, search_info):
    for i in range(len(find_list) - 1):
        if search_info == find_list[i]:
            return i
    return None


if __name__ == '__main__':
    list_a = [i for i in range(10000000)]
    find_num = random.randint(0, 10000000)
    print("find number is : {}".format(find_num))
    t1 = time.time()
    binary_search(list_a, find_num)
    t2 = time.time()
    easy_search(list_a, find_num)
    t3 = time.time()
    cost_1 = t2 - t1
    cost_2 = t3 - t2
    print("二分法查找花费时间:{}".format(cost_1))
    print("简单查询法查找花费时间:{}".format(cost_2))

运行结果:

>>>
find number is : 8645596
二分法查找花费时间:0.0
简单查询法查找花费时间:0.3390543460845947

一些常见的大 O 运行时间

O( log n ) , 也叫对数时间, 这样的算法包括二分查找

O( n ) , 也叫线性时间, 这样的算法包括简单查找

O( n * log n ) , 这样的算法包括快速排序法

O( n^2 ) , 这样的算法包括 冒泡排序 插入排序 选择排序

O( n! ), 旅行家算法

O( log n ) 比 O( n ) 快, n 越大, 前者比后者就快的越多.

选择排序

数组和链表

链表: 链表中的元素可以储存在内存的任何地方. 链表的每个元素都存储了下一个元素的地址, 从而使一系列随机的内存地址串在一起. 只要有足够的内存空间, 就能为链表分配内存.链表的优势在插入元素,删除元素方面.

数组: 数组在内存中都是相连的,添加元素可能需要开辟新的内存空间将所有元素转移,对于数组,我们知道其中每个元素的地址. 数组的优势在查询方面.

**结论:**数组的元素都在一起, 链表的元素是分开的, 其中每个元素都存储下一个元素的地址. 数组的读取速度很快, 链表的插入和删除速度很快.

选择排序

以下代码为冒泡, 插入, 选择 排序示例

# –*– coding: utf-8 –*–
# @Time : 2019/1/4 20:24
# @Author : Damon_duanlei
# @FileName : sort_methord.py
# @BlogsAddr : https://blog.csdn.net/Damon_duanlei
import random
import copy
import time


def bubble_sort(sort_list):
    for i in range(len(sort_list) - 1):
        for j in range(len(sort_list) - 1 - i):
            if sort_list[j] > sort_list[j + 1]:
                sort_list[j], sort_list[j + 1] = sort_list[j + 1], sort_list[j]


def insert_sort(sort_list):
    for i in range(1, len(sort_list)):
        j = i
        insert_num = sort_list[i]
        while insert_num < sort_list[j - 1]:
            sort_list[j] = sort_list[j - 1]
            j -= 1
            if j == 0:
                sort_list[0] = insert_num
                break
            if insert_num >= sort_list[j - 1]:
                sort_list[j] = insert_num


def selection_sort(sort_list):
    new_list = []
    while len(sort_list):
        index = 0
        min_num = sort_list[0]
        for i in range(1, len(sort_list)):
            if min_num > sort_list[i]:
                min_num = sort_list[i]
                index = i
        new_list.append(sort_list.pop(index))
    return new_list


if __name__ == '__main__':
    list_a = [random.randint(0, 100) for i in range(20000)]
    list_1 = copy.deepcopy(list_a)
    list_2 = copy.deepcopy(list_a)
    list_3 = copy.deepcopy(list_a)
    t1 = time.time()
    bubble_sort(list_1)
    t2 = time.time()
    insert_sort(list_2)
    t3 = time.time()
    new_list = selection_sort(list_3)
    t4 = time.time()
    print("冒泡排序用时: {}".format(t2 - t1))
    print("插入排序用时: {}".format(t3 - t2))
    print("选择排序用时: {}".format(t4 - t3))

运行结果:

>>>
冒泡排序用时: 27.151382446289062
插入排序用时: 20.87420392036438
选择排序用时: 7.898883581161499

以上三种排序方法的时间复杂度均为 O( n^2 ) 但是同样的元素排序后花费的时间出现的较大差异, 这与大 O 表示法中的常数有关, 后续部分将详细解释.

第三章 递归

递归

函数自己调用自己的过程

递归只是让解决方案更清晰, 并咩有性能上的优势. 甚至有些情况下, 使用循环的性能更好.

基线条件(递归出口)和递归条件

由于递归函数调用自己, 因此编写这样的函数很容易出错, 进而导致无线循环. 编写递归函数时, 必须告诉它何时停止递归. 正因为如此,每个递归函数都用两部分: 基线条件和递归条件. 递归条件指的是函数调用自己, 而基线条件则指的是函数不再调用自己, 从而避免形成无限循环.

栈(stack)又名堆栈,它是一种运算受限的线性表。其限制是仅允许在表的一端进行插入和删除运算。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。

计算机在内部使用被称为调用栈的栈.计算机如何使用调用栈,代码示例如下:

# –*– coding: utf-8 –*–
# @Time : 2019/1/6 17:05
# @Author : Damon_duanlei
# @FileName : demon_01.py
# @BlogsAddr : https://blog.csdn.net/Damon_duanlei


def greet(name):
    print("hello, {} !".format(name))
    greet2(name)
    print("getting ready to say bye ...")
    bye()


def greet2(name):
    print("how are you {} ?".format(name))
    

def bye():
    print("good bye !")


if __name__ == '__main__':
    greet("Demon")

过程:

当你调用 greet(“damon”) , 计算机首先为该函数调用分配一块内存. 我们使用这些内存. 变量 name 被设置成 Damon 并存储到内存中. 每当调用函数时, 计算机都会想这样将函数调用涉及的所有变量的值存储到内存中.

接下来, 打印 hello Damon ! , 在调用greet2(“Damon”). 同样, 计算机也为这个函数调用分配一块内存.

计算机使用一个栈来表示这些内存块, 其中第二个内存块位于第一个内存块上面. 当打印 how are you Demon ? ,然后从函数调用返回.此时, 栈顶的内存块被弹出.

此时, 栈顶的内存块是函数greet的, 这意味着运行返回到了函数greet. 当调用函数greet2时, 函数 greet 只执行了一部分. 调用一个函数是, 当前函数暂停并处于未完成状态, 该函数的所有变量的值都还在内存中. 执行完greet2后, 回到函数 greet ,并从离开的地方开始接着往下执行: 首先打印 getting ready to say bye … ,在调用bye.

在栈顶添加了函数bye的内存块. 然后打印 good bye ! ,并从这个函数返回. 当回到函数 greet. 由于没有别的事情要做, 就从函数 greet 返回. 这个栈用于存储多个函数的变量, 被称为调用栈

递归调用栈

递归函数也使用调用栈! 过程和普通行数调用相同区别只是函数多次调用自己.栈在递归中扮演着重要角色. 使用栈很方便,但是也要付出代价: 存储详尽的信息可能占用大量的内存. 每个函数调用都占用一定的内存, 如果栈很高, 就意味着计算机存储了大量函数调用的信息. 在这种情况下,有两种选择: 1. 重新编写代码,转而使用循环 2.使用尾递归

以下代码为使用递归方式去斐波那锲数列

# –*– coding: utf-8 –*–
# @Time : 2019/1/6 17:39
# @Author : Damon_duanlei
# @FileName : Fibomacci.py
# @BlogsAddr : https://blog.csdn.net/Damon_duanlei
import time


def fibomacci(n):
    if n == 0:
        return 0
    if n == 1:
        return 1
    ret = fibomacci(n - 1) + fibomacci(n - 2)
    return ret


fibomacci_list = []
for i in range(50):
    t1 = time.time()
    fibomacci_list.append(fibomacci(i))
    t2 = time.time()
    print("第{}位数计算话费时间: {} 秒".format(i + 1, t2 - t1))
print(fibomacci_list)

当 n 取值超过 30 时,函数执行效率明显下降.

下一章快速排序 学习进行中…

谢谢

点赞