算法基础

算法

算法(Algorithm):一个计算过程,解决问题的方法。

时间复杂度

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

当循环少一半的时候 时间复杂度O(logn)
几次关于n的循环就是n的几次方的复杂度

print('Hello World')           #假如说这行代码运行时间是一个单位O(1)

for i in range(n):             # 这段代码的时间是O(n),因为执行了n次
    print('Hello World')   

for i in range(n):             # 这段代码是O(n*n),因为在执行了n*n次
    for j in range(n):        
        print('Hello World')

for i in range(n):             #这代码是O(n*n*n),执行了n的立方次
    for j in range(n):       
        for k in range(n):          
            print('Hello World')

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

常见的时间复杂度(按效率排序): O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n2logn)<O(n3)

 

空间复杂度

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

空间换时间:多给它一些空间或内存,让它运行速度更快

 

递归

特点:

  • 1.调用自身
  • 2.有结束条件
def func(x):    
    if x>0:       
        print(x)       
        func(x-1)
print(4)
# 打印结果 4 3 2 1
#因为先打印再递归
def func(x):
    if x > 0:
        func(x-1)
        print(x)
func(4)
# 打印结果 1 2 3 4
# 因为先递归,再打印

那么递归前先打印,和递归后再打印有什么不同?

 

如果先递归再打印,那么打印的内容先不被执行,直到递归跳出的时候才从内到外的进行打印.

 

打印抱着抱着抱着我的小可爱的我的我的我

def text(n):
    if n>0:
        print('抱着',end='')
        text(n-1)
        print('的我',end='')

    else:
        print('我的小可爱',end='')

text(3)

 

二分查找

关键点:

候选区data[0:n]

def bin_search(li, val):         # li是传入的列表 val是要查找的值
    low = 0                      # low是起始的索引值
    high = len(li) - 1           # high是末尾的索引值
    while low <= high:           # 满足起始索引小于末尾索引的条件就执行循环
        mid = (low + high) // 2  # mid是列表的中间数的索引
        if li[mid] == val:       # 正好找到要查找的值的索引
            return mid
        elif li[mid] < val:      # 中间数的值小于被查找的值
            low = mid + 1        # 说明val在中间数的右边
        else:
            high = mid - 1       # 说明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

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

输出查询的列表值的下标

既然二分查找那么快为什么不全部都用二分查找呢? 

因为二分查找的前提是列表是有序的,那么如何让无序的列表变成有序才是最大的问题.

列表排序

冒泡排序

冒泡排序:两层遍历,相邻的两个值,如果左边的数大于右边的数,则交换位置。

def bubble_sort(li):
    '''冒泡排序'''
    for i in range(0,len(li)-1):
        exchange=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] # 交换位置
                exchange=True  # 优化,如果不为true说明了没有进行交换,列表是有序的
        if not exchange:
            return
li=list(range(0,10000))
random.shuffle(li)
bubble_sort(li)
print(li)

 

时间复杂度为O(n**2)

选择排序

选择排序:遍历一趟记录最小的数,放到第一位。再遍历剩下的数,找的最小的数,继续放置。

关键点:无序区和最小数的位置。

def select_sort(li):
    '''
    选择排序,比较无序区最小的一个,放在有序曲后一个
    :param li: 列表
    :return:
    '''
    for i in range(0,len(li)-1):
        min_local=i
        for j in range(i+1,len(li)):
            if li[min_local] > li[j]:
                min_local=j
        li[i],li[min_local]=li[min_local],li[i]

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

时间复杂度为O(n**2)

插入排序

列表被分为有序区和无序区两个部分。最初有序区只有一个元素。

每次从无序区选择一个元素,插入到有序区的位置,直到无序区变空。

代码关键点:如何找到无序区数,如何插到有序区中。

def inser_sort(li):
    '''插入排序,
        从i开始表示摸牌的位置,
        与前面的数进行相比,如果比摸的牌大则挪一位'''
    for i in range(1,len(li)):
        j=i-1
        temp=li[i]
        while j>=0 and li[j]>temp:
            li[j+1] = li[j]
            j-=1
        li[j+1]=temp

li=list(range(0,10000))
random.shuffle(li)
inser_sort(li)
print(li)

类似于摸牌然后插入

时间复杂度为O(n**2)

 

快速排序

快速排序:取一个元素p(第一个元素),使元素p归位,列表被p元素分为两部分,左边都比p小,右边都比p大,然后递归完成排序。

import random
def partition(li, left, right):
    i=random.randint(left,right)
    li[left],li[i] = li[i],li[left]
    tem=li[left]
    while left < right:  # 列表里至少两个元素才满足这个条件
        while left < right and li[right] >= tem:  # 从右边边找小于tem的数
            right -= 1      # 如果不小于,继续找
        li[left]= li[right] # 找到比tem小的数,挪到左边
        while left < right and li[left] <= tem:  # 从左边找比tem大的数
            left += 1
        li[right]= li[left]
    li[left] = tem
    return left  # 这里返回left和right是一样的
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=list(range(10000,0,-1))
random.shuffle(li)

ret=quick_sort(li,0,len(li)-1)
print(li)

快速排序的时间复杂度 为O(nlogn)

 

堆排序

知识点介绍

  • 根节点、叶子节点
    • 没有父亲的节点叫根节点,比如A, 没有孩子的节点叫叶子节点,比如BCHPQ等等
  • 树的深度
    • 有多少层,图示是4层
  • 树的度
    • 树的度势节点度的最大值比如A的度是6,E的度是2,F的度是3
  • 孩子节点/父节点
  • 子树
    • 在同一个分支上的是子树,只有一个的也是子树

 

 

二叉树

度不超过2的树(节点最多有两个)

 

  • 满二叉树:一个二叉树
    • 如果每一层的节点都达到了最大值,则这个二叉树就是满二叉树
  • 完全二叉树
    • 叶节点只能出现在最下层和次下层,并且最下层的节点都集中在最左边的若干位置的二叉树

 

父亲节点和左孩子节点有什么位置关系?

0-1 1-3 2-5 3-7 4-9

i=2i+1

右父亲节点和孩子节点有什么位置关系?

0-2 1-4 2-6 3-8 3-10

i=2i+2

  • 大根堆
    • 一颗完全二叉树,满足任一节点都比孩子大
  • 小根堆
    • 一颗完全二叉树,满足任一节点都比孩子小

 

如何构建一个堆

挨个出数

  • 结果就是成立一个新的堆

 再然后就是把8放到有序列表中,把最后的子节点替代根节点然后向下调整

 

堆排序的代码 

def sift(li,low,high):
    temp=li[low] #根节点
    i=low
    j=2* i + 1      # i是他的孩纸坐标
    while j <= high:
        if j < high and li[j+1]>li[j]: #如果右孩子存在,且右孩子的值大于左孩子
            j+=1

        if temp<li[j]:     #如果根节点的值小于他的孩子
            li[i] = li[j]  # 将孩子放置到根节点上
            i=j            #更新i的坐标值
            j= 2* i +1     #更新i的坐标值

        else:           # 如果根节点大于两个孩子节点
            li[i]=temp  #放置在根节点上 结束.
            break
    else:
        li[i] = temp     # 第二种跳出情况,temp找到自己的位置跳出,放置到i


def heap_sort(li):
    n=len(li)
    # 1.建堆的过程
    for i  in range(n//2-1,-1,-1): # 最后一个非叶子节点的位置是n//2 -1
        sift(li,i,n-1)
    # 2. 诶个出数
    for i in range(n-1,-1,-1):
        li[0],li[i]=li[i],li[0]
        sift(li,0,i-1)

import random
li=list(range(0,100000))
random.shuffle(li)
heap_sort(li)
print(li)

 归并排序

归并算法的核心就是拆分列表,在合并的过程中进行排序

def merge(li, low, mid, high):  # high是右边片段最后一个数的索引
    li_tem = []
    i = low  # low是左边片段第一个数的索引
    j = mid + 1  # mid是左边片段最大的数的索引
    while i <= mid and j <= high:  # 如果左右两个片段都有值,就继续取值。只要其中一个片段没值,就跳出循环。
        if li[i] < li[j]:  # 如果左边片段的最小值小于右边的最小值
            li_tem.append(li[i])  # 就将最小的那个值取出来放到一个列表中。
            i += 1  # 然后继续比较剩下的最小值
        else:  # 反之,就是右边的最小值小于左边的最小值
            li_tem.append(li[j])
            j += 1  # 然后继续比较剩下的最小值
    # 跳出第一个while循环的条件是:如果左边的片段取完了,其索引i不小于mid了
    # 如果右边的片段取完了,其索引j就不小于high了
    # 下面的两个while循环只可能有一个执行
    while i <= mid:  # 这里是左偏片段还有值
        li_tem.append(li[i])
        i += 1  # 继续添加到列表中
    while j <= high:  # 这里是右边片段还有值
        li_tem.append(li[j])
        j += 1  # 继续添加到列表中
    li[low:high+1] = li_tem


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 = [2, 5, 7, 8, 9, 1, 3, 4, 6, 10]
merge_sort(li, 0, len(li)-1)
print(li)

后三种算法总结

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

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

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

三种排序算法的缺点:

  • 快速排序:极端情况下,排序效率低
  • 归并排序:需要额外的内存开销
  • 堆排序:在快的排序算法中相对较慢

那么稳定与不稳定是什么呢?

稳定的排序是指在排序相同数字的时候不打乱原有的顺序,

比如[ (1,'alex') ,(1,'egon'),(2,'ming')  ]

在排序的时候不打乱原有的数据顺序.

[ (1,'egon'),(1,'alex') ,,(2,'ming')]如果不确定是否会打乱原有顺序的则是不稳定的.

相邻比较的都是稳定的.

 

posted @ 2018-08-12 16:24  R00M  阅读(347)  评论(0编辑  收藏  举报