01:常用算法

算法其他篇

目录:

算法刷题网站: https://leetcode-cn.com/problemset/all/

1.1 常用查找方法     返回顶部

   1、递归

    1. 递归条件

        1、 自己调用自己
        2、 有结束条件

  2、二分查找

l = list(range(1,101))
def bin_search(data_set,val):
   low = 0
   high = len(data_set) - 1
   while low <= high:
      mid = (low+high)//2
      if data_set[mid] == val:
         return mid
      elif data_set[mid] < val:
         low = mid + 1
      else:
         high = mid - 1
   return
n = bin_search(l,11)
print(n)            # 返回结果是: 10
二分查找对1~100乱序数字查找

1.2 列表排序常用方法介绍     返回顶部

  1、常用排序方法

    1、 性能最差的三个排序
      1) 冒泡排序
      2) 选择排序
      3) 插入排序
    2、 快速排序
    3、 排序NB二人组
      1) 堆排序
      2) 归并排序

  2、时间复杂度

      1、循环减半的过程复杂度 O(logn)
      2、几次循环就是n的几次方的复杂度

  3、排序方法比较

                

1.3 性能最差的三个排序     返回顶部

  1、冒泡排序代码(最好是O(n), 最坏O(n2))  

      原理:拿自己与上面一个比较,如果上面一个比自己小就将自己和上面一个调换位置,依次再与上面一个比较,第
              一轮结束后最上面那个一定是最大的数

import random
def bubble_sort(li):
   for i in range(len(li) - 1):
      exchange = False
      for j in range(len(li) - i -1):  #内层for循环执行一次,选出一个最大值,将可以调换位置的数调整
         if li[j] > li[j + 1]:
            li[j],li[j+1] = li[j+1],li[j]
            exchange = True
      if not exchange:                # 如果上一趟没有发生交换就证明已经排序完成
         break
data = list(range(100))
random.shuffle(data)                  #将有序列表打乱
bubble_sort(data)
print(data)
冒泡排序
#! /usr/bin/env pythonf
# -*- coding: utf-8 -*-
def bubble_sort(li):
    for i in range(len(li)-1):
        for j in range(len(li)-i-1):
            if li[j] > li[j+1]:
                li[j],li[j+1]=li[j+1],li[j]

li = [1,5,2,6,3,7,4,8,9,0]
bubble_sort(li)
print(li)               # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
冒泡排序精简版

  2、选择排序

      1、先假定第一个是最小的,依次与其他数比,如果其他数中有比第一个数小就假定这个更小的最小
      2、再比,第一轮就可以找到最小的那个放到0号位置,然后在假定1号位置数最小与剩下比较,再找到第二小的数放到第1号位置

import random
def select_sort(li):
   for i in range(len(li) - 1):
      min_loc = i                #开始先假设0号位置的值最小
      for j in range(i+1, len(li)):      #循环无序区,依次比较,小于min_loc就暂定他的下标最小
         if li[j] < li[min_loc]:        #所以内层for循环每执行一次就选出一个小值
            min_loc = j
      li[i], li[min_loc] = li[min_loc],li[i]
data = list(range(100))
random.shuffle(data)        #将有序列表打乱
select_sort(data)
print(data)
选择排序
import random
def select_sort(li):
   for i in range(len(li) - 1):
      min_loc = i                        #开始先假设0号位置的值最小
      for j in range(i+1, len(li)):      #循环无序区,依次比较,小于min_loc就暂定他的下标最小
         if li[j] < li[min_loc]:         #所以内层for循环每执行一次就选出一个小值
            min_loc = j
      li[i], li[min_loc] = li[min_loc],li[i]
       
li = [1,5,2,6,3,7,4,8,9,0]
select_sort(li)
print(li)               # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
选择排序精简版

  3、插入排序(比如码牌)

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

import random

def insert_sort(li):
   for i in range(1, len(li)):
      tmp = li[i]     #tmp是无序区取出的一个数
      j = i - 1       #li[j]是有序区最大的那个数
      while j >= 0 and li[j] > tmp:
         # li[j]是有序区最大的数,tmp是无序区取出的一个数,tmp从有序区最大的那个数开始比
         # 小就调换位置,直到找到有序区中值不大于tmp的结束
         li[j+1]=li[j]    #将有序区最右边的数向右移一个位置
         j = j - 1
      li[j + 1] = tmp       #将tmp放到以前有序区最大数的位置,再依次与前一个数比较
data = list(range(100))
random.shuffle(data)        #将有序列表打乱
insert_sort(data)
print(data)
插入排序

1.4 快排:快速排序中最简单的(递归调用)     返回顶部

  注:倒序,和 列表中有大量重复元素时,时间复杂度很大

  1、快排例子

      注:快排代码实现(类似于二叉树 递归调用)----右手左手一个慢动作,右手左手一个慢动作重播

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import random
import sys
sys.setrecursionlimit(10000000)             #设置系统最大递归深度

def quick_sort(data, left, right):
    if left < right:
        mid = partition(data, left, right)    # mid返回的是上一个用来排序那个数的下标
        quick_sort(data, left, mid - 1)
        quick_sort(data, mid + 1,right)

# 每执行一次partition函数都可以实现将某个数左边都比这个数小右边都比这个数大
def partition(data, left, right):
    tmp = data[left]
    while left < right:
        while left < right and data[right] >= tmp:     # 从右向左找小于tmp的数放到左边空位置
            right -= 1
        data[left] = data[right]                       # 将右边小于tmp值得数放到左边空位置
        while left < right and data[left] <= tmp:      # 从左向右找到大于tmp的值放到右边空位置
            left += 1
        data[right] = data[left]                       # 将右边大于tmp值得数放到右边空位置
    data[left] = tmp
    return left

data = list(range(100))
random.shuffle(data)                                 #将有序列表打乱
quick_sort(data, 0, len(data) - 1)
print(data)
快排
#! /usr/bin/env python
# -*- coding: utf-8 -*-
def quick_sort(arr):
    '''''
    模拟栈操作实现非递归的快速排序
    '''
    if len(arr) < 2:
        return arr
    stack = []
    stack.append(len(arr)-1)
    stack.append(0)
    while stack:
        l = stack.pop()
        r = stack.pop()
        index = partition(arr, l, r)
        if l < index - 1:
            stack.append(index - 1)
            stack.append(l)
        if r > index + 1:
            stack.append(r)
            stack.append(index + 1)


def partition(arr, start, end):
    # 分区操作,返回基准线下标
    pivot = arr[start]
    while start < end:
        while start < end and arr[end] >= pivot:
            end -= 1
        arr[start] = arr[end]
        while start < end and arr[start] <= pivot:
            start += 1
        arr[end] = arr[start]
    # 此时start = end
    arr[start] = pivot
    return start

lst = [1,3,5,7,9,2,4,6,8,10]
quick_sort(lst)
print lst   # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
不使用递归实现快排
#! /usr/bin/env python
# -*- coding: utf-8 -*-
def quick(list):
    if len(list) < 2:
        return list

    tmp = list[0]  # 临时变量 可以取随机值
    left = [x for x in list[1:] if x <= tmp]  # 左列表
    right = [x for x in list[1:] if x > tmp]  # 右列表
    return quick(left) + [tmp] + quick(right)

li = [4,3,7,5,8,2]
print quick(li)  # [2, 3, 4, 5, 7, 8]

#### 对[4,3,7,5,8,2]排序
'''
[3, 2] + [4] + [7, 5, 8]                 # tmp = [4]
[2] + [3] + [4] + [7, 5, 8]              # tmp = [3] 此时对[3, 2]这个列表进行排序
[2] + [3] + [4] + [5] + [7] + [8]        # tmp = [7] 此时对[7, 5, 8]这个列表进行排序
'''
快排简易实现

  2、快排原理

              

# 从排序前--------> 到P归位 经历过程(前面都比5小后面都比5大)
# 1、    首先从右向左比较,取出列表第一个元素5(第一个位置就空出来)与列表最后一个元素8比较,8>5不换位置
# 2、    用5与-2位置的9比,5<9不换位置
# 3、    5与-3位置的2比较,2<5,将-3位置的5放到1号位置,那么-3号位置空出来了,然后从左往右比较
# 4、    5与2号位置的7比,5<7,将7放到-3号位置,2号位置空出来了,在从右往左比
# 5、    -4号位置的1小于5将1放到空出的2号位置,-4位置空出来了,再从右向左比
# 6、    这样第一次循环就实现了5放到列表中间,前面的都比5大,后面的都比5小
快排思路详解

  3、快排与冒泡时间复杂度对比

 

最好情况

一般情况

最坏情况

快排

O(nlogn)

O(nlogn)

O(n^2)

冒泡

O(n)

O(n^2)

O(n^2)

  4、快排最坏时间复杂度为何为O(n2)

      1. 每次划分只能将序列分为一个元素与其他元素两部分,这时的快速排序退化为冒泡排序

      2. 如果用数画出来,得到的将会是一棵单斜树,也就是说所有所有的节点只有左(右)节点的树;平均时间复杂度O(n*logn)

1.5 堆排序     返回顶部

  1、堆的定义:http://www.cnblogs.com/MOBIN/p/5374217.html  

      1、堆中某个节点的值总是不大于或不小于其父节点的值;

      2、堆总是一棵完全二叉树

      3、完全二叉树定义:

        1)若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数
        2)第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。

      4、完全二叉树特性

        1)一个高度为h的完全二叉树最多有  2-1 个节点

        2)根为 i 号节点,左孩子 为 2i、 右孩子为 2i+1,父亲节点 (i – 1) / 2

        3)一个满二叉树 第 m层节点个数 等于 2m-1

        4)推倒一个h层的满二叉树为何 有 2h -1 个节点

          s = 20 + 21 + 22  +  23 + ...... + 2h-1

               s = 21 + 21 + 22 + 23 ...... + 2h-1 - 1

          s = 2*21 + 22 + 23 ...... + 2h-1 - 1

          s = 22 + 22 + 23 ...... + 2h-1 - 1

          s = 2*22 + 23 ...... + 2h-1 - 1

          s = 2h -1

  2、调长定义(节点的左右子树都是堆但自己不是堆)

    1. 调长图解

                      

    2. 调长原理

        1、首先将2拿出来与9和7比,这里面9最大,就用9作为根
        2、2放到9以前的位置,与8和5比,8最大放到开始9的位置
        3、2放到起始8的位置与6和4比,6最大,就出现了右边那张图了

  3、构造堆从最后一个有孩子的父亲开始 

#! /usr/bin/env python
# -*- coding: utf-8 -*-
def sift(data, low, high):
   '''  构造堆  堆定义:堆中某节点的值总是不大于或不小于父节点的值
   :param data: 传入的待排序的列表
   :param low:  需要进行排序的那个小堆的根对应的号
   :param high: 需要进行排序那个小堆最大的那个号
   :return:
   '''
   i = low            #i最开始创建堆时是最后一个有孩子的父亲对应根的号
   j = 2 * i+ 1       #j子堆左孩子对应的号
   tmp = data[i]      #tmp是子堆中原本根的值(拿出最高领导)
   while j <= high:  #只要没到子堆的最后(每次向下找一层)  #孩子在堆里
      # if j < high and data[j] < data[j + 1]:
      if j + 1 <= high and data[j] < data[j + 1]: #如果有右孩纸,且比左孩子大
         j += 1
      if tmp < data[j]:        #如果孩子还比子堆原有根的值tmp大,就将孩子放到子堆的根
         data[i] = data[j]     #孩子成为子堆的根
         i = j                 #孩子成为新父亲(向下再找一层)
         j = 2 * i + 1         #新孩子  (此时如果j<=high证明还有孩,继续找)
      else:
         break                 #如果能干就跳出循环就会流出一个空位
   data[i] = tmp                #最高领导放到父亲位置

def heap_sort(data):
   '''调整堆'''
   n = len(data)
   # n//2-1 就是最后一个有孩子的父亲那个子堆根的位置
   for i in range(n // 2 - 1, -1, -1):  #开始位置,结束位置, 步长       这个for循环构建堆
      # for循环输出的是: (n // 2 - 1 ) ~ 0 之间的数
      sift(data, i , n-1)     # i是子堆的根,n-1是堆中最后一个元素


data = [20,50,20,60,70,10,80,30,40]
heap_sort(data)
print data  # [80, 70, 20, 60, 50, 10, 20, 30, 40]
构造堆

      1、在构造有序堆时,我们开始只需要扫描一半的元素(n/2-1 ~ 0)即可,为什么?
      2、因为(n/2-1)~0的节点才有子节点,如图1,n=8,(n/2-1) = 3 即3 2 1 0这个四个节点才有子节点
      3、所以代码4~6行for循环的作用就是将3 2 1 0这四个节点从下到上,从右到左的与它自己的子节点比较并调整最终形成大顶堆,过程如下:
      4、第一次for循环将节点3和它的子节点7 8的元素进行比较,最大者作为父节点(即元素60作为父节点)

                           

      5、第二次for循环将节点2和它的子节点5 6的元素进行比较,最大者为父节点(元素80作为父节点)

                          

      6、第三次for循环将节点1和它的子节点3 4的元素进行比较,最大者为父节点(元素70作为父节点)

                            

      7、第四次for循环将节点0和它的子节点1 2的元素进行比较,最大者为父节点(元素80作为父节点)

                        

          注:元素20和元素80交换后,20所在的节点还有子节点,所以还要再和它的子节点5 6的元素进行比较,这就是28行代码 i = j 的原因

      8、至此有序堆已经构造好了!如上面右图

                        

  4、调整堆

    1. 调整堆过程

        1、建立堆
        2、通过调长,得到堆顶元素,为最大元素
        3、去掉堆顶,将最后一个元素放到堆顶,此时可通过一次调整重新使堆有序
        4、堆顶元素为第二大元素
        5、重复步骤3,直到堆变空

    2. 调整堆具体步骤

      1、 堆顶元素80和尾40交换后-->调整堆

                                  

      2、堆顶元素70和尾30交换后-->调整堆

                              

      3、堆顶元素60尾元素20交换后-->调整堆

                           

      4、其他依次类推,最终已排好序的元素如下

                          

  5、堆排序代码实现

 

# !/usr/bin/env python
# -*- coding:utf-8 -*-
import random

def sift(data, low, high):
    '''  构造堆  堆定义:堆中某节点的值总是不大于或不小于父节点的值
    :param data: 传入的待排序的列表
    :param low:  需要进行排序的那个小堆的根对应的号
    :param high: 需要进行排序那个小堆最大的那个号
    :return:
    '''
    root = low  # root最开始创建堆时是最后一个有孩子的父亲对应根的号
    child = 2 * root + 1  # child子堆左孩子对应的号
    tmp = data[root]  # tmp是子堆中原本根的值(拿出最高领导)
    while child <= high:  # 只要没到子堆的最后(每次向下找一层)  #孩子在堆里
        if child + 1 <= high and data[child] < data[child + 1]:  # 如果有右孩纸,且比左孩子大
            child += 1
        if tmp < data[child]:  # 如果孩子还比子堆原有根的值tmp大,就将孩子放到子堆的根
            data[root] = data[child]  # 孩子成为子堆的根
            root = child  # 孩子成为新父亲(向下再找一层)
            child = 2 * root + 1  # 新孩子  (此时如果child<=high证明还有孩,继续找)
        else:
            break  # 如果能干就跳出循环就会流出一个空位
    data[root] = tmp  # 最高领导放到父亲位置

def heap_sort(data):
    '''调整堆'''
    n = len(data)
    ''' n//2-1 就是最后一个有孩子的父亲那个子堆根的位置 '''
    for i in range(n // 2 - 1, -1, -1):  # 开始位置,结束位置, 步长       这个for循环构建堆
        # for循环输出的是: (n // 2 - 1 ) ~ 0 之间的数
        sift(data, i, n - 1)  # i是子堆的根,n-1是堆中最后一个元素
    # 堆建好了,后下面就是挨个出数
    for i in range(n - 1, -1, -1):  # i指向堆的最后        这个for循环出数然后,调长调整堆
        # for循环输出的是 : n-1 ~ 0之间所有的数,n-1就是这个堆最后那个数的位置
        data[0], data[i] = data[i], data[0]  # 将堆的第一个和最后一个值调换位置(将最大数放到最后)
        sift(data, 0, i - 1)  # 将出数后的部分重新构建堆(调长)


data = list(range(100))
random.shuffle(data)  # 将有序列表打乱
heap_sort(data)
print(data)
堆排

 

  6、初始化建堆过程时间:O(n) 公式推倒

    参考博客:https://www.cnblogs.com/GHzz/p/9635161.html

    说明:建堆时间复杂度指初始化堆需要调整父节点和子节点顺序次数

''' 假设高度为:k '''
#### 1、推倒第i层的总时间:s = 2^( i - 1 )  *  ( k - i )
# 说明:如果在最差的条件下,就是比较次数后还要交换;因为这个是常数,所以提出来后可以忽略;
'''
1. 2^( i - 1):表示该层上有多少个元素
2. ( k - i):表示子树上要下调比较的次数:第一层节点需要调整(h-1)次,最下层非叶子节点需要调整1次。
3. 推倒
    倒数第1层下调次数:s = 2^( i - 1 )  *  0 
    倒数第2层下调次数:s = 2^( i - 1 )  *  1
    倒数第3层下调次数:s = 2^( i - 1 )  *  2
    倒数第i层下调次数:s = 2^( i - 1 )  *  ( k - i )
'''

#### 2、一次新建堆总时间:S = n - longn -1  # 根据1中公式带人推倒
# S = 2^(k-2) * 1 + 2^(k-3)*2.....+2*(k-2)+2^(0)*(k-1)  ===> 因为叶子层不用交换,所以i从 k-1 开始到 1;
'''
S = 2^(k-2) * 1 + 2^(k-3)*2.....+2*(k-2)+2^(0)*(k-1)     # 等式左右乘上2,然后和原来的等式相减,就变成了:
S = 2^(k - 1) + 2^(k - 2) + 2^(k - 3) ..... + 2 - (k-1)
S = 2^k -k -1                                            # 又因为k为完全二叉树的深度,所以 
(2^(k-1)) <=  n < (2^k - 1 )                             # 两边同时对2取对数,简单可得
k = logn                                                 # 实际计算得到应该是 log(n+1) < k <= logn 

综上所述得到:S = n - longn -1,所以时间复杂度为:O(n)
'''
推导初始化对时间复杂度:O(n)

  7、堆排序时间:O(nlogn) 公式推倒

    1)推导方法1:

      循环  n -1 次,每次都是从根节点往下循环查找,所以每一次时间是 logn,总时间:logn(n-1) = nlogn  - logn ;

    2)推导方法2:

      1. 在一个堆中一次调长(调整堆)时间复杂度: log(n)

      2. 排序时一次出栈顶元素需要循环 n次,每次时间复杂度为:log(n)

      3. 所以总时间复杂度:nlog(n)

1.6 归并排序(递归调用)      返回顶部

  1、归并原理图

                 

  2、归并排序代码(时间复杂度:O(nlogn))

#! /usr/bin/env python
# -*- coding: utf-8 -*-
def merge(li, low, mid, high):
   '''
   :param li:      带排序列表
   :param low:     列表中第一个元素下标,一般是:0
   :param mid:     列表中间位置下标
   :param high:    列表最后位置下标
   :return:
   '''
   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 mergesort(li, low, high):
   if low < high:
      mid = (low + high) // 2          #获取列表中间的索引下标
      mergesort(li, low, mid)          #先分解
      mergesort(li, mid+1, high)
      merge(li, low, mid, high)        #然后合并

data = [10,4,6,3,8,2,5,7]
mergesort(data, 0 , len(data) -1)
print(data)                            # [2, 4, 6, 8, 10, 12, 14, 16, 18]
归并排序

1.7 快速排序,堆排序, 归并排序 比较     返回顶部

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

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

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

  3、三种排序算法的缺点

      1、快速排序: 极端情况下排序效率低( O(n2) )
      2、归并排序: 需要额外内存开销(需要新建一个列表放排序的元素)
      3、堆排序: 在快的排序算法中相对较慢,堆排序最稳定

1.8 时间复杂度、空间复杂度和稳定性     返回顶部

  1、各种算法比较

     

  2、算法不稳定定义 

      定义:在排序之前,有两个数相等,但是在排序结束之后,它们两个有可能改变顺序.

      说明:在一个待排序队列中,A和B相等,且A排在B的前面,而排序之后,A排在了B的后面.这个时候,我们说这种算法是不稳定的.

  3、不稳定的几种算法

    1)快排为什么不稳定

        3 2 2 4 经过第一次快排后结果:2 2 3 4 (第3号位置的2第一次排序后跑到第1号位置了)

    2)堆排序为什么不稳定  

        如果堆顶3先输出,则,第三层的27(最后一个27)跑到堆顶,然后堆稳定,继续输出堆顶,是刚才那个27

        这样说明后面的27先于第二个位置的27输出,不稳定

        

    3)选择排序为什么不稳定

        5 8 5 2 9 第一次假定1号位置的5最小,但是实际最小的是4号位置的2

        第一次排序后为:2 8 5 5 9 以前1号位置的5跑到3号位置5的后面了

   

 

posted @ 2018-03-15 14:46  不做大哥好多年  阅读(776)  评论(0编辑  收藏  举报