常用算法(二)—高级算法

快速排序(quick sort)

首先任意选取一个数据(通常选用数组的第一个数)作为关键数据,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序。

将数组分割成两个数组之后再分别对剩下的两个数组执行排序,这样循环,直到剩一个元素。

import time,random
import copy

def cal_time(func):          #该装饰器用来测量函数运行的时间
    def wrapper(*args,**kwargs):
        t1 = time.time()
        res = func(*args,**kwargs)
        t2 = time.time()
        print("%s running time: %s secs." % (func.__name__, t2 - t1))
        return res
    return wrapper

def query_sort(data,left,right):
    if left < right:
        mid = partition(data,left,right)
        query_sort(data,left,mid-1)
        query_sort(data,mid+1,right)

def partition(data,left,right):
    temp = data[left]
    while left < right:
        while left<right and data[right] >= temp:
            right -=1
        data[left] = data[right]
        while left<right and data[left] <= temp:
            left +=1
        data[right] = data[left]
    data[left] = temp
    return left

@cal_time
def query_sort_h(data):
    query_sort(data,0,len(data)-1)

@cal_time
def sort_fun(data):
    data.sort()
data = list(range(10000))
random.shuffle(data)
data1 = copy.deepcopy(data)
print(data)
query_sort_h(data)
sort_fun(data1)
print(data)

  

二叉树

树的特征和定义


  树是一种重要的非线性 数据结构,直观地看,它是 数据元素(在树中称为结点)按分支关系组织起来的结构,很象自然界中的树那样。 树结构在客观世界中广泛存在,如人类社会的族谱和各种社会组织机构都可用树形象表示。树在计算机领域中也得到广泛应用,如在编译源程序时,可用树表示源程序的语法结构。又如在 数据库系统中,树型结构也是信息的重要组织形式之一。一切具有层次关系的问题都可用树来描述。
 

树(Tree)是元素的集合。我们先以比较直观的方式介绍树。下面的数据结构是一个树:

树有多个节点(node),用以储存元素。某些节点之间存在一定的关系,用连线表示,连线称为边(edge)。边的上端节点称为父节点,下端称为子节点。树像是一个不断分叉的树根。

每个节点可以有多个子节点(children),而该节点是相应子节点的父节点(parent)。比如说,3,5是6的子节点,6是3,5的父节点;1,8,7是3的子节点, 3是1,8,7的父节点。树有一个没有父节点的节点,称为根节点(root),如图中的6。没有子节点的节点称为叶节点(leaf),比如图中的1,8,9,5节点。从图中还可以看到,上面的树总共有4个层次,6位于第一层,9位于第四层。树中节点的最大层次被称为深度。也就是说,该树的深度(depth)为4。

 

如果我们从节点3开始向下看,而忽略其它部分。那么我们看到的是一个以节点3为根节点的树:

三角形代表一棵树

再进一步,如果我们定义孤立的一个节点也是一棵树的话,原来的树就可以表示为根节点和子树(subtree)的关系:

 

上述观察实际上给了我们一种严格的定义树的方法:

1. 树是元素的集合。

2. 该集合可以为空。这时树中没有元素,我们称树为空树 (empty tree)。

3. 如果该集合不为空,那么该集合有一个根节点,以及0个或者多个子树。根节点与它的子树的根节点用一个边(edge)相连。

上面的第三点是以递归的方式来定义树,也就是在定义树的过程中使用了树自身(子树)。由于树的递归特征,许多树相关的操作也可以方便的使用递归实现。我们将在后面看到。

树的实现

树的示意图已经给出了树的一种内存实现方式: 每个节点储存元素和多个指向子节点的指针。然而,子节点数目是不确定的。一个父节点可能有大量的子节点,而另一个父节点可能只有一个子节点,而树的增删节点操作会让子节点的数目发生进一步的变化。这种不确定性就可能带来大量的内存相关操作,并且容易造成内存的浪费。

一种经典的实现方式如下:

 二叉树: 

二叉树是由n(n≥0)个结点组成的有限集合、每个结点最多有两个子树的有序树。它或者是空集,或者是由一个根和称为左、右子树的两个不相交的二叉树组成。

特点:

(1)二叉树是有序树,即使只有一个子树,也必须区分左、右子树;

(2)二叉树的每个结点的度不能大于2,只能取0、1、2三者之一;

(3)二叉树中所有结点的形态有5种:空结点、无左右子树的结点、只有左子树的结点、只有右子树的结点和具有左右子树的结点。

 

二叉树(binary)是一种特殊的树。二叉树的每个节点最多只能有2个子节点:

二叉树

由于二叉树的子节点数目确定,所以可以直接采用上图方式在内存中实现。每个节点有一个左子节点(left children)和右子节点(right children)。左子节点是左子树的根节点,右子节点是右子树的根节点。

 

如果我们给二叉树加一个额外的条件,就可以得到一种被称作二叉搜索树(binary search tree)的特殊二叉树。二叉搜索树要求:每个节点都不比它左子树的任意元素小,而且不比它的右子树的任意元素大。

(如果我们假设树中没有重复的元素,那么上述要求可以写成:每个节点比它左子树的任意节点大,而且比它右子树的任意节点小)

二叉搜索树,注意树中元素的大小

二叉搜索树可以方便的实现搜索算法。在搜索元素x的时候,我们可以将x和根节点比较:

1. 如果x等于根节点,那么找到x,停止搜索 (终止条件)

2. 如果x小于根节点,那么搜索左子树

3. 如果x大于根节点,那么搜索右子树

二叉搜索树所需要进行的操作次数最多与树的深度相等。n个节点的二叉搜索树的深度最多为n,最少为log(n)。

 

二叉树的遍历

遍历即将树的所有结点访问且仅访问一次。按照根节点位置的不同分为前序遍历,中序遍历,后序遍历。

前序遍历:根节点->左子树->右子树

中序遍历:左子树->根节点->右子树

后序遍历:左子树->右子树->根节点

例如:求下面树的三种遍历

 

前序遍历:abdefgc

中序遍历:debgfac

后序遍历:edgfbca

 

 二叉树的类型

(1) 完全二叉树——若设二叉树的高度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第h层有 叶子结点,并且叶子结点都是从左到右依次排布,这就是 完全二叉树
(2) 满二叉树——除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树。
(3)平衡二叉树——平衡二叉树又被称为AVL树(区别于AVL算法),它是一棵二叉排序树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树

如何判断一棵树是完全二叉树?按照定义,

教材上的说法:一个深度为k,节点个数为 2^k - 1 的二叉树为满二叉树。这个概念很好理解,

就是一棵树,深度为k,并且没有空位。

首先对满二叉树按照广度优先遍历(从左到右)的顺序进行编号。

一颗深度为k二叉树,有n个节点,然后,也对这棵树进行编号,如果所有的编号都和满二叉树对应,那么这棵树是完全二叉树。

 

image

堆排序

堆排序,顾名思义,就是基于堆。因此先来介绍一下堆的概念。
堆分为最大堆和最小堆,其实就是完全二叉树。最大堆要求节点的元素都要大于其孩子,最小堆要求节点元素都小于其左右孩子,两者对左右孩子的大小关系不做任何要求,其实很好理解。有了上面的定义,我们可以得知,处于最大堆的根节点的元素一定是这个堆中的最大值。其实我们的堆排序算法就是抓住了堆的这一特点,每次都取堆顶的元素,将其放在序列最后面,然后将剩余的元素重新调整为最大堆,依次类推,最终得到排序的序列。

 

堆排序就是把堆顶的最大数取出,

将剩余的堆继续调整为最大堆,具体过程在第二块有介绍,以递归实现

剩余部分调整为最大堆后,再次将堆顶的最大数取出,再将剩余部分调整为最大堆,这个过程持续到剩余数只有一个时结束

import time,random,copy
from cal_time import cal_time,nor_sort

def sift(data,low,high):   #来一次调整
    i= low         #low为根节点,父节点
    j= 2 * i + 1    #i的左孩子
    tmp = data[i]
    while j <= high: #没有超过数组范围时
        if j < high and data[j] < data[j + 1 ]:#如果j小于high也就是说左孩子小于最边界的high那么这个节点肯定有右孩子,
            j +=1   #这个时候对比左孩子和右孩子的大小,如果右孩子大于左孩子,那么这个时候把J置为右孩子,J的左右就是表示两个孩子里面最大的那个
        if tmp < data[j]: #如果父亲的值小于孩子的值,这个时候开始换位置
            data[i] = data[j]    #j的值换为父亲,i的值换为孩子
            i =j             #把I的位置调整到之前比父亲大的那个孩子的位置,
            j = 2* i + 1     #开始比较当前这个孩子和他自己孩子的大小,找到它的左孩子,定义为j
        else:     #如果两个孩子都没父亲大
            break    #什么也不干,跳出当前循环
    data[i] = tmp    #把最初始i的值赋给调整之后的位置

@cal_time
def heap_sort(data):
    n = len(data)
    for i in range(n //2 -1,-1,-1):#取最后一个有孩子的父节点,倒序遍历
        sift(data,i,n-1)  #调整
    for i in range(n-1,-1,-1):
        data[0],data[i] = data[i],data[0]
        sift(data,0,i-1)

data = list(range(10000))
random.shuffle(data)
data1 = copy.deepcopy(data)
print(data)
heap_sort(data)
print(data)
data1 = nor_sort(data1)
print(data1)

 

归并排序

假设现在的列表分两段有序,如何将其合成为一个有序列表

将序列垂直分割成两个有序序列,然后取每个序列的第一项对比,小的取出来然后继续对比,当某一序列取完之后就把另一个序列剩下的全部放到新的序列里面

将数组细分,先分解到数个有序的序列,比如只包含一个元素的序列是有序的,然后再开始合并,所以称该算法为归并排序。

代码示例:

import time,random,copy
from cal_time import cal_time,nor_sort

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

def _merge_sort(data,low,high):
    if low<high:
        mid = (low+high)//2
        _merge_sort(data,low,mid)
        _merge_sort(data,mid+1,high)
        merge(data,low,mid,high)

@cal_time
def merge_sort(data):
    _merge_sort(data,0,len(data)-1)

data = list(range(10000))
random.shuffle(data)
data1 = copy.deepcopy(data)
print(data)
merge_sort(data)
nor_sort(data1)
print(data)

  

 快速排序、堆排序、归并排序的区别

三种排序算法的时间复杂度都是O(nlogn) 一般情况下,就运行时间而言: 快速排序 < 归并排序 < 堆排序

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

希尔排序(shell sort)

希尔排序(Shell Sort)是插入排序的一种。也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本,该方法的基本思想是:先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率比直接插入排序有较大提高,希尔排序每趟并不使某些元素有序,而是使整体数据越来越接近有序;最后一趟排序使得所有数据有序。

首先要明确一下增量的取法:

      第一次增量的取法为: d=count/2;

      第二次增量的取法为:  d=(count/2)/2;

      最后一直到: d=1;

看上图观测的现象为:

        d=3时:将40跟50比,因50大,不交换。

                   将20跟30比,因30大,不交换。

                   将80跟60比,因60小,交换。

        d=2时:将40跟60比,不交换,拿60跟30比交换,此时交换后的30又比前面的40小,又要将40和30交换,如上图。

                   将20跟50比,不交换,继续将50跟80比,不交换。

        d=1时:这时就是前面讲的插入排序了,不过此时的序列已经差不多有序了,所以给插入排序带来了很大的性能提高。

代码示例:

import random,copy
from cal_time import cal_time,nor_sort

def shell_sort(data):
    gap = len(data) // 2
    print(gap)
    while gap >=1:
        for i in range(gap,len(data)):
            tmp = data[i]
            j = i-gap
            while j >= 0 and tmp < data[j]:
                data[j + gap] = data[j]
                j -=gap
            data[i-gap] = tmp
        gap //=2

data = list(range(10000))
random.shuffle(data)
data1 = copy.deepcopy(data)
print(data)
shell_sort(data)
print(data)
data1 = nor_sort(data1)
print(data1)

  

各排序方法总结:

 备注:1、深度递归调用有时会出错需要设置递归保护代码:

sys.setrecursionlimit(100000)

 2、切记不能给递归函数加装饰器,这样的话每次递归装饰器都会执行一次

转载于:https://www.cnblogs.com/qiangayz/p/9344608.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值