算法(Algorithm)基础:Python 3.x实现

目录

  • 1、算法概念相关

  • 2、查找算法

  • 2.1 线性查找

  • 2.2 二分查找

  • 3、排序算法

  • 3.1 插入排序

  • 3.2 快速排序

  • 3.3 选择排序

  • 3.4 冒泡排序

  • 3.5 归并排序

  • 3.6 堆排序

  • 3.7 计数排序

  • 3.8 希尔排序

  • 3.9 拓扑排序

正文

1、算法相关概念

程序 = 算法 + 数据结构

程序:算法用某种程序设计语言的具体实现,程序可以不满足有穷性。

算法:指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制

算法只不过是流程或菜谱的时髦说法,详尽地描述了如何完成某项任务。 请看下面的菜谱:西红柿炒鸡蛋
1)先取一些西红柿、鸡蛋;
2)切好西红柿、打好鸡蛋;
3)打开电磁炉,先将鸡蛋煎好;
4)再加入西红柿;
5)如果喜欢吃甜的,加些糖;
6)煮熟为止;
7)记得每隔3分钟检查一次。



这个菜谱并不神奇,但其结构很有启发性。它由一系列必须按顺序执行的操作说明组成,其中有些可直接完成(取些西红柿和鸡蛋),有些需要特别注意(如果喜欢吃甜的),还有一些需要重复多次(每隔3分钟检查一次)。


菜谱和算法都由原料(对象)操作说明(语句)组成。在这个示例中,西红柿和鸡蛋是原料,而操作说明包括切西红柿、打鸡蛋、翻炒、烹饪指定的时间等。


这就是计算机的简单算法逻辑。

算法的描述形式 有:

  • 自然语言
  • 算法框图法
  • 伪代码语言
  • 高级程序设计语言

算法的 5个特征

  • 输入性:一个算法有0个 或多个输入,以刻画运算对象的初始情况。所谓0个输入是指算法本身定出了初始条件;
  • 输出性:一个算法有1个 或多个输出,以反映对输入数据加工后的结果。没有输出的算法是毫无意义的;
  • 确切性:算法中每条指令清晰,无歧义。即 算法的每一步骤必须有确切的定义;
  • 有穷性:指算法必须能在执行有限个步骤之后终止。算法中每条指令的执行次数有限,执行每条指令的时间也有限。
  • 可行性:算法中执行的任何计算步骤都是可以被分解为基本的可执行的操作步(比如 人们用纸和笔做有限次运算后即可完成)。即每个计算步都可以在有限时间内完成(也称之为 有效性)。

判定算法优劣 的5个标准:一个算法的评价主要从时间复杂度 和空间复杂度来考虑。

  • 正确性:在合理的数据输入下,能在有限时间内得出正确的结果,是评价一个算法优劣的最重要的标准
  • 可读性:易于人的理解,易于调试。
  • 健壮性:具备检查错误 和对错误进行适当处理的能力。即 一个算法对不合理数据输入的反应能力和处理能力,也称为容错性。
  • 效率:算法执行时所需计算机资源的多少,包括运行时间和存储空间。
  • 时间复杂度:指执行算法所需要的计算工作量。一般来说,计算机算法是问题规模n的函数f(n)一般,算法的时间复杂度也因此记做 T(n)=Ο(f(n))
    因此,问题的规模n越大,算法执行的时间的增长率与 f(n) 的增长率正相关,称作渐进时间复杂度(Asymptotic Time Complexity)
  • 空间复杂度:指算法需要消耗的内存空间。其计算和表示方法 与时间复杂度类似,一般都用复杂度的渐近性来表示。同时间复杂度相比,空间复杂度的分析要简单得多。

其中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间,时间复杂度 常用O表述,使用这种方式时,时间复杂度可被称为是渐近的,它考察当输入值大小趋近无穷时的情况。

时间复杂度是用来估计算法运行时间的一个式子(单位),一般来说,时间复杂度高的算法比复杂度低的算法慢。

print('Hello world')  # O(1) “循环” 1# O(1) 执行1次
print('Hello World')
print('Hello Python')
print('Hello Algorithm')

for i in range(n):  # O(n) “循环” n次
    print('Hello world')

for i in range(n):  # O(n^2) “循环” n^2for j in range(n):
        print('Hello world')

for i in range(n):  # O(n^2) “循环” n^2print('Hello World')
    for j in range(n):
        print('Hello World')

for i in range(n):  # O(n^2) “循环” n^2for j in range(i):
        print('Hello World')

for i in range(n):# O(n^3) “循环” n^3for j in range(n):
        for k in range(n):
            print('Hello World')

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

n = 64
while n > 1:#循环 6次 O(6print(n)
    n = n // 2

其中,2^6 = 64O(f(n)) = log2(64) = 6。所以循环减半的时间复杂度为O(log2(n)),即O(logn)

常见的时间复杂度高低排序:O(1)

O(1):常数型 
O(log2 n):对数型 
O(n):线性型 
O(nlog2n):二维型 
O(n^2):平方型 
O(n^3):立方型 
O(2^n):指数型

算法的空间复杂度 用来评估算法内存占用大小的一个式子。
定义一个或多个变量,空间复杂度都是为1,列表的空间复杂度为列表的长度。

# 空间复杂度为1
a = 'Python'

num1 = [1, 2, 3, 4, 5]  # 空间复杂度为5

num2 = [[1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]]  # 空间复杂度为5*4

num = [[[1, 2], [1, 2]], [[1, 2], [1, 2]] , [[1, 2], [1, 2]]]  # 空间复杂度为3*2*2

算法设计的一般过程:

  • 1、理解问题
  • 2、预测所有可能是输入
  • 3、在精确解 和近似解间做选择
  • 4、确定适当的数据结构
  • 5、算法设计技术
  • 6、描述算法
  • 7、跟踪算法
  • 8、分析算法的效率
  • 9、根据算法编写代码

2、查找

2.1 线性查找

也称 线性搜索(或 顺序查找)。从第一个元素m开始逐个与需要查找的元素x进行比较,当比较到元素值相同(即m=x)时返回元素m的下标,如果比较到最后都没有找到,则返回-1。速度最慢,但是适用性最广。

适合于存储结构为 顺序存储 或链式存储的线性表。


缺点:是当n 很大时,平均查找长度较大,效率低;
优点:是对表中数据元素的存储没有要求。另外,对于线性链表,只能进行顺序查找。

这是 最基础的遍历无序列表的查找算法。
算法(Algorithm)基础:Python 3.x实现_第1张图片

[root@master linearsearch]# cat ls.py
#coding=utf-8

def linear_Search(arr, n, x):
        for i in range(0, n):
                if arr[i] == x:
                        return i
        return -1;

arr = ['A', 'B', 'C', 'D', 'E']
x = 'D'
n = len(arr)

result = linear_Search(arr, n, x)
if result == -1:
        print('未找到!')
else:
        print('找到!%s在数组中的索引为%s' %(x, result))
[root@master linearsearch]# python3 ls.py
找到!D在数组中的索引为3

上述这个算法的 时间复杂度分析:效率

查找成功时的平均查找长度为:(假设每个数据元素的概率相等)ASL = 1/n(1+2+3+…+n) = (n+1)/2
  当查找不成功时,需要n+1次比较,时间复杂度为O(n)
  所以,顺序查找的时间复杂度为O(n)

2.2 二分查找

算法(Algorithm)基础:Python 3.x实现_第2张图片
二分查找是一种在有序数组(也可以是有序的列表 或其他数据类型)中查找某一特定元素的查找算法。

  • 查找过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则查找过程结束;
  • 如果某一特定元素大于或者小于中间元素,则在数组大于或者小于中间元素那一半中查找,而且跟开始一样从中间元素开始比较;
  • 如果在某一步骤数组为空,则代表找不到
  • 这种查找算法每一次比较都使查找范围缩小一半

输入 要查找的目标,比如 1
最后得到:1 在什么位置(得到一个索引)

有时候,在实际生产中可能并不需要知道它在哪个位置,只是确认它在里面。

【递归版】

[root@master algorithm]# pwd
/usr/local/src/badou/code/algorithm
[root@master algorithm]# cat binarySearch.py        
#coding=utf-8

import sys

def binary_search(list_args, left, right, target, count):
        if left > right:#递归结束条件
                return [-1, count]

        count += 1#进行二分的次数
        mid = (left + right) // 2 #地板除法:整数除法,返回不大于结果的最大整数
        if target < list_args[mid]:#目标小于中间位置元素,只需再比较左边的元素
                right = mid - 1
        elif target > list_args[mid]:#目标大于中间位置元素,只需再比较右边的元素
                left = mid + 1
        else:#目标正好等于中间位置元素,函数结束,返回值
                return [mid, count]
        #print('left:%d, right:%d' %(left, right))

        return binary_search(list_args, left, right, target, count)

list_args = [1, 3, 4, 6, 7, 8, 10, 13, 14]
#若list_args非顺序,则做一个处理
#list_args = list_args.sort()

target = int(sys.argv[1])
res = binary_search(list_args, 0, len(list_args)-1, target, 0)

if res[0] == -1:
        print('未找到!需查找的目标是%s,共进行了%d次二分.' % (target,res[1]))
else:
        print('找到!需要查找的目标%s在索引位置%d,共进行了%d次二分.' %(target,res[0],res[1]))

执行:

[root@master algorithm]# python3 binarySearch.py 10
找到!需要查找的目标10在索引位置6,共进行了2次二分.
[root@master algorithm]# python3 binarySearch.py 7
找到!需要查找的目标7在索引位置4,共进行了1次二分.
[root@master algorithm]# python3 binarySearch.py 66
未找到!需查找的目标是66,共进行了4次二分.

二分查找算法的时间复杂度:
假设共有n个元素,每次查找区间大小依次是:

n
n/2
n/4
...
n/(2^k)
其中,k为循环次数

因为n/(2^k) 向上取整 ≥1

>>> import math
>>> math.ceil(4/5)
1

计算时间复杂度是按照最坏的情况进行计算,即 在排除到只剩下最后一个值之后得到结果:
所以,令n/(2^k) = 1(最差的情况,即区间大小为1),那么k = log2(n)(以2为底,n的对数)。
因此:时间复杂度可表示为 O(f(n)) = O(log2(n)),在数据结构这样写 O(logn)

二分查找算法(递归版)的空间复杂度:每次递归所开空间 * 深度 = O(log2(n))

参考:
分析时间复杂度&空间复杂度,以二分查找和斐波那契数的递归和非递归算法为例

BFPRT算法,也称 中位数的中位数算法

3、排序

排序算法的思想就是将一堆无序的数据采用某一种方法让它变得有序,而通常由于数据的组合形式不同,采用不同的方法有助于提升排序效率,这就是为什么会有这么多的排序算法的原因。

排序算法(sort algorithm)有:插入排序、快速排序、选择排序、冒泡排序、归并排序、堆排序、计数排序、希尔排序、拓扑排序。

3.1 插入排序


上述动图:

  • 黄色块:表示经过for遍历的
  • 浅蓝色块:表示未经for遍历的
  • 绿色块:表示在移动,正经历while循环
  • 红色块:表示当前目标

插入排序(Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列从后向前扫描,找到相应位置并插入。

输入:是一个无序的数据
输出:是一个有序的数据

[root@master algorithm]# cat insertsort.py
#coding=utf-8

def insertSort(data):
        for i in range(1, len(data)):#从索引1开始,因为索引0前面没有元素,不用比较。
                key = data[i] #获取目标
                j = i - 1#获取目标前一个的索引 以便获取它的值
                while j >= 0 and key < data[j]:#满足两个条件:1 目标前一个的索引得大于等于0,否则无意义;2 目标 小于前一个值。若不满足这两个条件,则结束while循环,目标待在原位置。
                        data[j+1] = data[j]#换位置:将目标换到 前一个位置上
                        j -= 1#获取目标换位置后的前一个位置的索引,以便下一轮循环跟前一个比较大小。接着检查while循环条件,若为True,即目标继续比前一个小,则继续换位置,直到前面都更小(即 while循环为False结束循环,进入下一行执行语句)。
                data[j+1] = key#将目标放在 当前位置,进入下一轮for循环,遍历下一个元素
        return data#返回经过上述循环处理后的结果

data = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]
sort_data = insertSort(data)

print(sort_data)
[root@master algorithm]# python3 insertsort.py
[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]

3.2 快速排序(quick sort)

快速排序使用分治法(Divide and conquer)策略。由图灵奖得主C. A. R. Hoare(托尼·霍尔)在1962年提出。

基本思想:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都 比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

步骤:【原理】

  • 1)挑选基准值:从数列中选择一个元素(可随意选,也可选中间那个元素便于理解),作为"基准"(pivot)(比较值);
  • 2)分割:重新排序数列,所有比基准值小的元素摆放在基准前面【左边】;所有比基准值大的元素摆在基准后面【右边】(与基准值相等的数可以到任何一边)。在这个分割结束之后,对基准值的排序就已经完成;
  • 3)递归排序子序列:以基准值左右两边的子列作为新数列,不断重复第一步和第二步,直到所有子集只剩下一个元素为止。

递归到最底部的判断条件是数列的大小是零或一,此时该数列显然已经有序。
选取基准值有多种具体方法,此选取方法对排序的时间性能有决定性影响。

简例:[10, 7, 8, 9, 1, 5]
使用quick sort的详细步骤:

  • 1)选择 8作为基准值
  • 2)从列表第1个元素10开始和基准值8进行比较,大于基准值,将其放入右边的分区中;第2个元素7比基准值8小,放入左边分区中;直到最后一个元素
  • 3)依次对左右两个分区进行再分区,直到最后一个元素
  • 4)分解完成再一层一层返回,返回规则是:左边分区+基准值+右边分区

代码:去中间位置的元素作为基准值

[root@master algorithm]# cat quick_sort_demo.py
#-*-coding:utf-8-*-

def quick_sort(lst):
    """快速排序"""

    #若列表只有1个元素,直接返回它自己。这也是递归结束条件
    if len(lst) < 2:
        return lst

    #step1:选取基准值。在此选取中间位置的元素,便于理解
    pivot = lst[len(lst) //2]

    #定义基准值左、右两个列表
    left,right = [],[]

    #从原始列表中移除基准值
    lst.remove(pivot)

    #step2:分割
    for item in lst:
        #大于基准值的元素放在右边(默认与基准值相等的元素放在右边)
        if item >= pivot:
            right.append(item)
        else:
            #小于基准值的元素放在左边
            left.append(item)

    #step3:递归排序子列表
    return quick_sort(left) + [pivot] +quick_sort(right)

data = [10, 7, 8, 9, 1, 5]
print(quick_sort(data))

[root@master algorithm]# python3 quick_sort_demo.py
[1, 5, 7, 8, 9, 10]

一行代码:

>>> quick_sort = lambda lst:lst if len(lst) < 2 else quick_sort([item for item in lst[1:] if item <= lst[0]]) +[lst[0]] +quick_sort([item for item in lst[1:] if item > lst[0]])
>>> data = [1, 5, 7, 8, 9, 10]
>>> quick_sort(data)
[1, 5, 7, 8, 9, 10]

快速排序的

  • 时间复杂度:O(nlogn)
  • 空间复杂度:排序时需要另外申请空间,并且随着数列规模增大而增大,其复杂度为O(nlogn)

下方展示 以最后一个元素作为基准值的代码示例:不过不好理解,可读性没那么好

[root@master algorithm]# cat quicksort.py
#-*-coding=utf-8-*-

#low --> 起始索引
#high--> 结束索引
#分区函数,分区的过程中始终对小于等于基准值的元素依次按照遍历顺序放置,得到分区后的新data
def partition(data, low, high):
        i = low -1#得到索引值low前面一个元素的索引值
        pivot = data[high]#将最后一个元素作为基准值

        for j in range(low, high):#不会遍历到最后一个元素(避开它,因为它是基准值)
                if data[j] <= pivot:#所遍历元素小于或等于【基准值】
                        i = i +1#表示小于等于基准值的元素个数
                        data[i],data[j] = data[j],data[i]#交换两个位置的值,即根据小于等于基准值的元素个数依次排好序。比如当前遍历的元素j是小于等于基准值的第二个,就将其排在i这个位置

        data[i+1],data[high] = data[high],data[i+1]#得到分区后的列表。即基准值放在用于分区的位置
        return i+1#得到用于划分分区的索引值

#快速排序函数
def quick_sort(data, low, high):
        if low < high:#递归结束条件是low>=high
                pi = partition(data, low, high)#划分分区,并得到分区后的data
                #分别对分区后的data 左右两部分进行再次分区
                quick_sort(data, low, pi-1)#对左边部分进行再分区
                quick_sort(data, pi+1, high)#对右边部分进行再分区

data = [10, 7, 8, 9, 1, 5]
n = len(data)
quick_sort(data, 0, n-1)

print(data)

[root@master algorithm]# python3 quicksort.py
[1, 5, 7, 8, 9, 10]

3.3 选择排序(selection sort)

原理或简单处理流程:

  • 1)从待排序序列中,找到最小元素;
  • 2)如果最小元素不是待排序序列的第一个元素,将其和第一个元素互换;每一次排序中,都将当前第 i 小的元素放在位置 i 上。
  • 3)从余下的n-1个元素中,找出最小元素。重复1)、2)步,直至排序结束。
[root@master algorithm]# cat select_sort.py 
#-*-coding=utf-8-*-

def select_sort(data):
    n = len(data)
    for i in range(n):#默认从0开始遍历
        min_idx = i#将i赋值给最小值索引
        for j in range(i+1, n):#从i+1开始遍历,比较大小
            if data[j] < data[min_idx]:#若小于min_idx,将索引j赋值给min_idx,直至遍历到最后一个元素,找到最小值
                min_idx = j
        data[i],data[min_idx] = data[min_idx],data[i]#将最小元素 跟索引i元素互换
    return data#返回排好序的data

data = [9,1,2,5,7,4,8,6,3,5]
print(select_sort(data))

[root@master algorithm]# python3 select_sort.py
[1, 2, 3, 4, 5, 5, 6, 7, 8, 9]

选择排序的:

  • 时间复杂度:选择排序的比较次数与序列的初始排序无关。 假设待排序的序列有n个元素,则比较次数总是n (n - 1) / 2
    而移动次数与序列的初始排序有关。当序列正序时,移动次数最少,为 0
    当序列反序时,移动次数最多,为3n (b - 1) / 2
    所以,综上所述,时间复杂度为O(n^2)
  • 空间复杂度:简单选择排序需要占用 1 个临时空间,在交换数值时使用,即 O(1)

3.4 冒泡排序(bubble sort)

通过比较两个相邻元素的大小实现排序,如果前一个元素大于后一个元素,就交换这两个元素。这样就会让每一趟冒泡都能找到最大一个元素并放到最后。

这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。

[root@master algorithm]# cat bubble_sort.py
#-*-coding=utf-8-*-

def bubble_sort(data):
    n = len(data)
    for i in range(n):#每次找出最大的元素放到最后
        for j in range(0, n-i-1):#只需比较剩余的(即前面的n-i个元素,最后一次比较是:第n-i-1个元素 与第n-i个元素比较)
            if data[j] > data[j+1]:#前者大于后者,则互换位置。将大于变成小于将变成冒泡排序(降序排列)
                data[j],data[j+1] = data[j+1],data[j]
    return data

data = [8, 1, 4, 6, 2, 3, 5, 7]
print(bubble_sort(data))

[root@master algorithm]# python3 bubble_sort.py
[1, 2, 3, 4, 5, 6, 7, 8]

冒泡排序的

  • 时间复杂度:由于嵌套了 2 层循环,故为 O(n^2);
  • 空间复杂度:由于整个排序过程是在原数据上进行操作,故为 O(1)

3.5 归并排序(merge sort)

1945年由约翰·冯·诺伊曼首次提出,创建在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。

归并排序的核心思想是利用递归与分治的技术将数据序列划分为越来越小的半子表,再对半子表排序,最后再用递归方法将排好序的半子表合并成越来越大的有序序列。通过递归,层层合并,即为归并。

  • 1)划分半子表-分
  • 2)合并半子表-合

算法导论中分冶策略定义:将原问题划分为n个规模较小,与原问题结构相似的子问题;递归解决这些子问题,然后再合并其结果,就得到原问题的解。

分冶模式三大步骤:

1,分解:将原问题分解为规模较小的n个子问题;

2,解决:递归得解决个子问题,若子问题足够小,则直接求解;

3,合并:将子问题合并为原问题的解;

[root@master algorithm]# cat merge_sort.py        
[root@master algorithm]# cat merge_sort.py
#-*-coding=utf-8-*-

def merge(left, right, data):#在此的left、right都已经是有序的
    """合并:在合并的过程中完成了排序"""
    i = j = 0#i 左边的索引值;j 右边的索引值。初始都为 0
    while i+j < len(data):
        #j==len(right)时,表示右边元素遍历完了;或 左边没有遍历完,同时左边小于右边
        if j == len(right) or (i < len(left) and left[i] < right[j]):
            data[i+j] = left[i]#将left[i]赋值给data[i+j]
            i += 1#将索引值加1,即接下来是遍历左边下一个元素了
        else:#右边元素小于左边
            data[i+j] = right[j]
            j += 1#将索引值加1,即接下来是遍历右边下一个元素了

def merge_sort(data):
    """归并排序"""
    n = len(data)

    #当data元素个数为10时,返回其本身即可。也是递归结束的条件
    if n < 2:
        return data

    """step1 分:平均切分为左右两个部分"""
    mid = n // 2
    left = data[0:mid]#左闭右开
    right = data[mid:n]

    #递归调用【merge_sort归并排序函数】:即 分别对左、右两部分 继续切分
    merge_sort(left)
    merge_sort(right)

    """step2 合(治 or 并):将左边两部分合并起来,合的过程进行排序"""
    merge(left, right, data)

data = [5, 4, 7, 9, 3, 8, 2, 1]
#data = [14, 2, 34, 43, 21, 19]
#data = [8, 4, 5, 7, 1, 3, 6, 2]

merge_sort(data)

print(data)

[root@master algorithm]# python3 merge_sort.py
[1, 2, 3, 4, 5, 7, 8, 9]

归并排序的:

  • 时间复杂度 O(n log n)
  • 空间复杂度:O(n),归并排序需要一个与原数组相同长度的数组做辅助来排序

参考:流程画的特别好
Python实现合并排序(归并排序)(一文看懂)
[图解] 归并排序

3.6 堆排序(heap sort)

是一种完全二叉树,且堆有两种类型:大根堆、小根堆

堆排序是利用 进行排序的。

堆排序的思想(以大根堆为例):

  • 1)将待排序的数据构造出一个大根堆
  • 2)取出这个大根堆的堆顶节点(最大值),与堆的最下 最右的元素进行交换。接着将剩下的元素再构造出一个大根堆
  • 3)重复第2)步,直至这个大根堆的长度为1,此时完成排序
[root@master algorithm]# cat heap_sort.py    
#-*-coding=utf-8-*-

def swap(data, root, last):#root,last代表索引值
    """交换:堆顶元素与最后元素"""
    data[root],data[last] = data[last],data[root]

def heap_adjust(data, par_node, high):
    """堆调整函数:调整父节点与子节点,生成大根堆"""
    new_par_node = par_node
    j = 2 * par_node +1#取得根节点的左子节点:若只有一个节点,则high为左子节点;若有两个子节点,则high为右子节点

    while j <= high:#若j、high相等,则表示无右子节点,即high为左子节点
        if j < high and data[j]<data[j+1]:#在此若不进行判断,则j<high可能超出索引
            #一个根节点下若有两个子节点,将j指向值大的节点
            j += 1
        if data[j] > data[new_par_node]:#若子节点值大于父节点,则互换
            data[new_par_node],data[j] = data[j],data[new_par_node]
            new_par_node = j#将当前节点作为父节点,查找它的子树
            j = j*2 +1
        else:
            #因为调整是从上到下,所以下面的所有子树肯定是排好序的
            #如果调整的父节点依然 比下面最大的子节点大,就直接打断循环,堆已调整好了
            break

def heap_sort(data):
    """
    堆排序函数

    索引计算:0 -->1 --> ...
    父节点 i
    左子节点 偶数 2i+1
    右子节点 奇数 2i+2
    注意:当用长度表示最后一个叶子节点时,得-1

    从第1个 非叶子节点(即 最后一个父节点)开始,即 
    开始循环到 root索引为0的第1个根节点,将所有的【根-叶子】调整好,成为一个大根堆
    
    根据data长度,找到最后一个非叶子节点,开始循环到root根节点,制作大根堆
    """
    length = len(data)
    last = length - 1#最后一个元素的索引
    last_par_node = (length // 2) -1

    while last_par_node >= 0:
        heap_adjust(data, last_par_node, length-1)
        last_par_node -= 1#每调整好一个节点,从后往前移动一个节点
    while last > 0:
        #swap(data, 0, last)
        data[0],data[last] = data[last],data[0]
        #调整堆 好让adjust处理最后已经排好序的数,就不处理了
        heap_adjust(data, 0, last-1)
        last -= 1
    return data

data = [4, 7, 0, 9, 1, 5, 3, 3, 2, 6]
heap_sort(data)
print(data)

[root@master algorithm]# python3 heap_sort.py
[0, 1, 2, 3, 3, 4, 5, 6, 7, 9]

参考:
堆排序的Python实现(附详细过程图和讲解)
堆排、python实现堆排
PYTHON实现堆排序:里面 有一个 打印堆排序使用 的函数非常6

堆排序的:

  • 时间复杂度:O(n log2 n)
  • 空间复杂度:O(1)

3.7 计数排序(count sort)

是一个非基于比较的排序算法,优势在于在对一定范围内的整数排序时,快于基于比较的排序算法。

算法思想
在于给定的输入序列中的每一个元素x,确定该序列中值小于等于x元素的个数,然后将x直接存放到最终的排序序列的正确位置上。

找出待排序的数组中最大和最小的元素;
统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。

[root@master algorithm]# cat count_sort.py
#-*- coding: utf-8 -*-

def count_sort(data):
    n = len(data)
    res = [None] *n

    #第1次遍历,每个元素的次数都统计
    for i in range(n):
        p = 0 #p表示a[i]大于列表其他元素的 次数
        q = 0 #q表示等于a[i]的次数

        for j in range(n):#第2次循环,列表中的每个元素 都和第1次循环的元素比较
            if data[i] > data[j]:
                p += 1
            elif data[i] == data[j]:
                q += 1
        for k in range(p, p+q):#q表示相等的次数,即从p开始索引后,连续p次,都是相同的数
            res[k] = data[i]
    return res

data = [4, 7, 0, 9, 1, 5, 3, 3, 2, 6]
print(count_sort(data))
[root@master algorithm]# python3 count_sort.py
[0, 1, 2, 3, 3, 4, 5, 6, 7, 9]

空间复杂度和时间复杂度

  • 假定原始数列的规模是N,最大值和最小值的差是M,计数排序的时间复杂度是O(N+M)
  • 如果不考虑结果数组,只考虑中间数组大小的话,空间复杂度是O(M)

局限性

  • 当数列的最大和最小值差距过大时,并不适用计数排序。
  • 当数列元素不是整数,并不适用计数排序。

3.8 希尔排序(shell sort)

希尔排序(Shell Sort)是插入排序的一种。也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。

该方法因DL.Shell于1959年提出而得名。 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的元素越来越多,当增量减至1时,整个数据恰被分成一组,算法便终止。

[root@master algorithm]# cat shell_sort.py
#-*-coding:utf-8-*-

def shell_sort(data):
    """希尔排序"""

    n = len(data)
    gap = n // 2

    while gap >= 1:
        for j in range(gap, n):
            i = j
            while  i-gap >= 0:
                if data[i] < data[i-gap]:
                    data[i],data[i-gap] = data[i-gap],data[i]
                    i -= gap
                else:
                    break
        gap //= 2

data = [4, 7, 0, 9, 1, 5, 3, 3, 2, 6]
shell_sort(data)
print(data)

[root@master algorithm]# python3 shell_sort.py
[0, 1, 2, 3, 3, 4, 5, 6, 7, 9]

时间复杂度:O(n^2)
空间复杂度:O(1)

3.9 拓扑排序

过程简述:

  • 1)从 DAG 图中选择一个 没有前驱(即入度为0)的顶点并输出。
  • 2)从图中删除该顶点和所有以它为起点的有向边。
  • 重复 1 和 2 直到当前的 DAG 图为空或当前图中不存在无前驱的顶点为止。若当前图中不存在无前驱的顶点说明有向图中必存在环。
[root@master algorithm]# cat topo_sort.py
#-*-coding:utf-8-*-

def topo_sort(graph):#如果没有现成的Graph,就要自己想办法造一个出来
    #创建入度字典
    in_degree = dict((u,0) for u in graph)#初始化所有顶点入度为0

    vertex_num = len(in_degree)

    #获取每个节点的入度
    for u in graph:
        for v in graph[u]:
            in_degree[v] += 1#计算每个顶点的入度,对应的key每在graph[u]里被提到一次,入度就+1

    #使用列表作为队列并将入度为0的添加到队列中
    q = [u for u in in_degree if in_degree[u] == 0]#筛选入度为0的顶点
    result = []

    #当队列中有元素时执行
    while q:
        u = q.pop()#默认从最后一个删除
        result.append(u)#将取出的元素存入结果中

        for v in graph[u]:
            in_degree[v] -= 1#类似之前,对应的key每在graph[u]里被提到一次,入度就-1
            if in_degree[v] == 0:
                q.append(v)#再次筛选入度为0的顶点

    if len(result) == vertex_num:#如果循环结束后存在非0入度的顶点说明图中有环,不存在拓扑排序
        return result
    else:
        print('this is a circle')

    return result

graph = {
    "A": ["B","C"],
    "B": ["D","E"],
    "C": ["D","E"],
    "D": ["F"],
    "E": ["F"],
    "F": [],
}

print(topo_sort(graph))

[root@master algorithm]# python3 topo_sort.py
['A', 'C', 'B', 'E', 'D', 'F']

参考:
python拓扑排序
Python图的拓扑排序
算法(Algorithm)基础:Python 3.x实现_第3张图片
算法的稳定性定义:对于待排序列中相同项的原来次序不能被算法改变则称该算法稳定。

菜鸟Python3 实例
Python算法基础:非常好,含时间、空间复杂度
Python之路:常用算法与设计模式:含时间复杂度
七大查找算法(Python)
十大编程算法助程序员走上高手之路:动态图做的非常好





你可能感兴趣的:(Hadoop大数据,Python,3.X,计算机理论与基础)