• 排序算法 和数据结构


    各种排序算法时间复杂度及空间复杂度对比

    Timsort与其他比较排序算法时间复杂度(time complexity)的比较

    空间复杂度(space complexities)比较

    各种排序算法

    同等硬件条件下,对相同长度的的列表排序,所有时常如下:

    # 打印结果:
    '''
    quick_sort:0.03600168228149414
    sorted:0.003000020980834961
    sorted:0.003000020980834961
    bubble_sort:11.830676555633545
    select_sort:7.196411609649658
    insert_sort:6.566375494003296
    shell_sort:0.06900382041931152
    count_sort:14.809847116470337
    merge_sort: 0.06600356101989746
    '''
    

    1.python内置方法sorted()和list.sort()排序

    内置数据类型list的方法sort(),内置函数sorted()
    这个的底层实现就是归并排序,只是使用了Python无法编写的底层实现,从而避免了Python本身附加的大量开销,速度比我们自己写的归并排序要快很多(10~20倍),所以说我们一般排序都尽量使用sorted和sort

    # Python内置函数sorted()
    li = [random.randint(0,100000) for i in range(10000)]
    start = time.time()
    sorted(li)
    print('sorted:%s' % (time.time() - start))
    
    # python内置数据类型list.sort()
    li = [random.randint(0,100000) for i in range(10000)]
    start = time.time()
    li.sort()
    print('sorted:%s' % (time.time() - start))
    
    '''
    sorted:0.003000020980834961
    sorted:0.003000020980834961
    '''
    

    2.冒泡排序

    # 冒泡排序
    # 最坏的时间复杂度是:(O(n^2)),最优的时间复杂度是:O(n)  有序的列表在内层循环一趟就就return了。
    def bubble_sort(l):
        for i in range(len(l)-1):
            # 定一个标志位,做一个排序的优化。如果在循环第一趟的时候,
            # 发现列表是一个有序的列表,就不会走内层循环里面的if条件
            # 判断里面的代码,进行交换数字,flag标志位就不会被置换
            # 成False,进而走内层循环外面的if判断,终止整个函数的运行。
    
            flag = True
            for j in range(len(l)-i-1):
                if l[j] > l[j+1]:
                    l[j],l[j+1] = l[j+1],l[j]
                    flag = False
            if flag:
                return
    # 冒泡排序
    li = [random.randint(0,100000) for i in range(10000)]
    start = time.time()
    bubble_sort(li)
    print('bubble_sort:%s' % (time.time() - start))
    
    
    '''
    bubble_sort:11.830676555633545
    '''
    

    3.选择排序

    # 选择排序
    def select_sort(l):
        for i in range(len(l)):
            minloc = i
            for j in range(i+1,len(l)):
                if l[j] < l[minloc]:
                    l[j],l[minloc] = l[minloc],l[j]
         return l
    # 选择排序
    li = [random.randint(0,100000) for i in range(10000)]
    start = time.time()
    select_sort(li)
    print('select_sort:%s' % (time.time() - start))
    
    '''
    select_sort:7.196411609649658
    '''
    

    4.插入排序

    # 插入排序
    # 时间复杂度 O(n^2)
    def insert_sort(l):
        for i in range(1,len(l)):
            j = i-1
            temp = l[i]
    
            while j >= 0 and l[j] > temp:
    
                l[j+1] = l[j]
                j = j-1
            l[j+1] = temp
    
    # l=[5,7,4,6,3,1,2,9,8]
    # insert_sort(l)
    # print(l)
    
    
    # 插入排序
    li = [random.randint(0,100000) for i in range(10000)]
    start = time.time()
    insert_sort(li)
    print('insert_sort:%s' % (time.time() - start))
    
    '''
    insert_sort:6.566375494003296
    '''
    

    5.快速排序

    def partition(l,left,right):
        tmp = l[left]
        while left < right:
            while left < right and l[right] >= tmp:
                right = right - 1
            l[left] = l[right]
    
            while left < right and l[left] <= tmp:
                left =left + 1
            l[right] = l[left]
    
        l[left] = tmp
        return left
    
    #快速排序
    # 时间复杂度:O(nlogn)
    def quick_sort(l,left,right):
        if left < right:
            mid = partition(l,left,right)
            quick_sort(l,left,mid-1)
            quick_sort(l,mid+1,right)
    
    # 快排
    li = [random.randint(0,100000) for i in range(10000)]
    start = time.time()
    quick_sort(li, 0, len(li)-1)
    print('quick_sort:%s' % (time.time() - start))
    
    '''
    quick_sort:0.03600168228149414
    '''
    

    6.希尔排序

    # 希尔排序
    '''
    希尔排序是一种分组插入排序算法
    首先取一个整数d1=n/2,将元素分为d1个组,魅族相邻元素之间的距离为d1,在各组内进行直接插入排序;
    取第二个整数d2=d1/2,重复上述分组排序过程,直到d1=1,即所有元素在同一组内进行直接插入排序。
    希尔排序每趟并不使某些元素有序,而是使整体数据越来越接近有序;最后一趟排序使得所有数据有序。
    '''
    
    def shell_sort(l):
        gap = len(l) // 2
        while gap > 0:
            for i in range(gap, len(l)):
                tmp = l[i]
                j = i - gap
                while j >= 0 and tmp < l[j]:
                    l[j + gap] = l[j]
                    j -= gap
    
                l[j + gap] = tmp
            gap //= 2
    # 希尔排序
    li = [random.randint(0,100000) for i in range(10000)]
    start = time.time()
    shell_sort(li)
    print('shell_sort:%s' % (time.time() - start))
    
    '''
    shell_sort:0.06900382041931152
    '''
    

    7.计数排序

    # 计数排序
    # 方式1: 时间复杂度O(n+k) k代表数组中的最大值  线性时间的排序
    # 优点:稳定,适用于最大值不是很大的整数序列,在k值较小时突破了基于比较的排序的算法下限
    # 缺点:存在前提条件,k值较大时,需要大量额外空间
    l = [3, 4, 4, 5, 5, 61, 1, 2, 2, 3, 3, 3, 3, 6, 7, 7, 7, 7, 8, 9,0]
    
    max_num = l[0]
    for num in l:
        if num > max_num:
            max_num = num
    l1 = [0] * (max_num +1)
    for num in l:
        l1[num] += 1
    l.clear()
    for i in range(len(l1)):
            while l1[i] > 0:
                l.append(i)
                l1[i] -= 1
    print(l)
    
    # 方式2:时间复杂度:O(n^2)
    def count_sort(l):
        n = len(l)
        res = [None] * n
        # 首次循环遍历, 每个列表的数都统计
        for i in range(n):
            # p 表示 a[i] 大于列表其他数 的次数
            p = 0
            # q 表示 等于 a[i] 的次数
            q = 0
            # 二次循环遍历, 列表中的每个数都和首次循环的数比较
            for j in range(n):
                if l[i] > l[j]:
                    p += 1
                elif l[i] == l[j]:
                    q += 1
            for k in range(p, p+q):  # q表示 相等的次数,就表示, 从 P 开始索引后, 连续 q 次,都是同样的 数
                res[k] = l[i]
        return res
    
    # 计数排序
    li = [random.randint(0,100000) for i in range(10000)]
    start = time.time()
    count_sort(li)
    print('count_sort:%s' % (time.time() - start))
    
    '''
    count_sort:14.809847116470337
    '''
    

    8.归并排序

    归并排序仍然是利用完全二叉树实现,它是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列。

    基本过程:假设初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到n/2个长度为2或1的有序子序列,再两两归并,最终得到一个长度为n的有序序列为止,这称为2路归并排序。

    下面的截图来自《大话数据结构》相关章节,便于理解整个代码的实现过程。

    图中主要表明了实例代码中的两部分,分别为原始序列的拆分和合并两部分。

    # 归并排序
    def merge_sort(lst):
        if len(lst) <= 1:
            return lst          # 从递归中返回长度为1的序列
    
        middle = len(lst) // 2
        left = merge_sort(lst[:middle])     # 通过不断递归,将原始序列拆分成n个小序列
        right = merge_sort(lst[middle:])
        return merge(left, right)
    
    def merge(left, right):
        i, j = 0, 0
        result = []
        while i < len(left) and j < len(right):  # 比较传入的两个子序列,对两个子序列进行排序
            if left[i] <= right[j]:
                result.append(left[i])
                i += 1
            else:
                result.append(right[j])
                j += 1
        result.extend(left[i:])         # 将排好序的子序列合并
        result.extend(right[j:])
        return result
    
    # 归并排序
    li = [random.randint(0, 100000) for i in range(10000)]
    start = time.time()
    lst = merge_sort(li)
    print("merge_sort:", time.time() - start)
    
    '''
    merge_sort: 0.06600356101989746
    '''
    

    9.桶排序

    优点:桶排序的时间复杂度是O(n+m),m是数列中最大值与最小值的插值,可实现快速排序。
    缺点:如果差值过大,内存消耗巨大

    
    #简单的桶排序
    def bucksort(A):
    
        bucks = dict()      # 定义一个桶变量,类型为字典
        for i in A:
            bucks.setdefault(i,[])  # 每个桶默认为空列表
            bucks[i].append(i)      # 往对应的桶中添加对应相同元素
        print(bucks)
        A_sort = []
        for i in range(min(A), max(A)+1):
            if i in bucks:                  # 检查是否存在对应数字的桶
                A_sort.extend(bucks[i])     # 合并桶中数据
        return A_sort
    
    A = [2,3,5,4,6,7,3,3,0,8,5]
    a = bucksort(A)
    print(a)
    
    # 包含其他元素的桶排序
    # element:人名,分数
    class Person:
        def __init__(self,name,score):
            self.name = name
            self.score = score
        def __repr__(self):     # 覆写打印输出 print Person()
            return self.name + "-" + str(self.score)
    
    
    def bucksort2(A):
        bucks = dict()
        for a in A:
            bucks.setdefault(a.score,[])  # 以个人分数为评价排列标准
            bucks[a.score].append(a)      # 在相同分数的桶中添加人
    
        A_sort = []
        scorelist = [a.score for a in A]    # 将人员列表中所有人的分数取出
        for i in range(min(scorelist), max(scorelist)+1):
            if i in bucks:
                A_sort.extend(bucks[i])
    
        return A_sort
    
    
    # 对人进行排序
    B = [Person('huhu',5),Person('haha',3),Person('xixi',5),Person('hengheng',2),Person('gaoshou',8)]
    b = bucksort2(B)
    print(b)
    '''
    [hengheng-2, haha-3, huhu-5, xixi-5, gaoshou-8]
    '''
    

    列表查找

    列表查找:从列表中查找指定元素

    ​ 输入:列表、待查找元素

    ​ 输出:元素下标或为查找到元素

    顺序查找:

    ​ 从列表的第一个元素开始,顺序进行搜索,直到找到为止。

    二分查找:

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

    # 二分查找
    def binary_search(l,low,high,value):
        mid = (low + high) // 2
        if low <= high:
            if l[mid] == value:
                return mid
            elif l[mid] > value:
                return binary_search(l,low,mid-1,value)
            else:
                return binary_search(l,mid+1,high,value)
    
        else:
            return "此数不存在!"
    
    l = [1,2,3,4,5,6,7,8,9,10,11,12,13]
    res = binary_search(l,0,len(l)-1,9)
    print(res)
    
    

    数据结构

    malloc()到底如何申请内存空间?

    malloc()到底从哪里得到了内存空间?
    
    答案是从堆里面获得空间。也就是说函数返回的指针是指向堆里面的一块内存。
    
          操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。
    
         malloc函数的实质体现在,它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表(Free List)。调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块(根据不同的算法而定(将最先找到的不小于申请的大小内存块分配给请求者,将最合适申请大小的空闲内存分配给请求者,或者是分配最大的空闲块内存块)。然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连接表上。
    
         调用free函数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了。于是,malloc函数请求延时,并开始在空闲链上翻箱倒柜地检查各内存片段,对它们进行整理,将相邻的小空闲块合并成较大的内存块。如果无法获得符合要求的内存块,malloc函数会返回NULL指针,因此在调用malloc动态申请内存块时,一定要进行返回值的判断。
    
    在此也要说明就是因为new和malloc需要符合大众的申请内存空间的要求,针对泛型提供的,分配内存设计到分配算法和查找,此外还要避免内存碎片,所以其效率比较低下,因此有时程序猿会自己重写new和delete,或者创建一个内存池来管理内存,提高程序运行的效率。
    

    数组和链表

    转至:https://blog.csdn.net/m0_37631322/article/details/81777855

    1.链表是什么

    链表是一种上一个元素的引用指向下一个元素的存储结构,链表通过指针来连接元素与元素;

    链表是线性表的一种,所谓的线性表包含顺序线性表和链表,顺序线性表是用数组实现的,在内存中有顺序排列,通过改变数组大小实现。而链表不是用顺序实现的,用指针实现,在内存中不连续。意思就是说,链表就是将一系列不连续的内存联系起来,将那种碎片内存进行合理的利用,解决空间的问题。

    所以,链表允许插入和删除表上任意位置上的节点,但是不允许随即存取。链表有很多种不同的类型:单向链表、双向链表及循环链表。

    2.单向链表

    单向链表包含两个域,一个是信息域,一个是指针域。也就是单向链表的节点被分成两部分,一部分是保存或显示关于节点的信息,第二部分存储下一个节点的地址,而最后一个节点则指向一个空值。

    3.双向链表

    从上图可以很清晰的看出,每个节点有2个链接,一个是指向前一个节点(当此链接为第一个链接时,指向的是空值或空列表),另一个则指向后一个节点(当此链接为最后一个链接时,指向的是空值或空列表)。意思就是说双向链表有2个指针,一个是指向前一个节点的指针,另一个则指向后一个节点的指针。

    4.循环链表

    循环链表就是首节点和末节点被连接在一起。循环链表中第一个节点之前就是最后一个节点,反之亦然。

    5.数组和链表的区别?

    不同:链表是链式的存储结构;数组是顺序的存储结构。

    链表通过指针来连接元素与元素,数组则是把所有元素按次序依次存储。

    链表的插入删除元素相对数组较为简单,不需要移动元素,且较为容易实现长度扩充,但是寻找某个元素较为困难;

    数组寻找某个元素较为简单,但插入与删除比较复杂,由于最大长度需要再编程一开始时指定,故当达到最大长度时,扩充长度不如链表方便。
    相同:两种结构均可实现数据的顺序存储,构造出来的模型呈线性结构。

    6.链表的应用、代码实践

    约瑟夫问题:

      传说在公园1世纪的犹太战争中,犹太约瑟夫是公元一世纪著名的历史学家。在罗马人占领乔塔帕特后,39 个犹太人与约瑟夫及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人俘虏,于是决定了一个流传千古的自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报到第3人该人就必须自杀,然后再由下一个人重新报数,直到所有人都自杀身亡为止。然而约瑟夫和他的朋友并不想遵从这个约定,约瑟夫要他的朋友先假装遵从,他将朋友与自己安排在第_个和第_个位置,于是逃过了这场死亡游戏,你知道安排在了第几个嘛?
    

    针对以上问题,使用单向循环链表的方式求解:

    # 控制参数:
    nums = 41
    call = 3
    # 参数定义:
    peoples = [True for _ in range(nums)]
    
    # append的方法性能不好
    # for _ in range(nums):
    #    peoples.append(True)
    
    result = []
    num = 1
    
    while (any(peoples)):
        for index, people in enumerate(peoples):
            if people:
                if num == call:
                    peoples[index] = False
                    result.append(index + 1)
                    #                print(index+1)#每轮的出局者
                    #                print(peoples)#每次的队列状态
                    num = 1
                else:
                    num += 1
    
    print('
    总数为%d,报数为%d' % (nums, call))
    print('约瑟夫序列为:
    %s
    ' % result)
    

    组织代码的方式要学习体会;

    7.自我理解

    1)数组便于查询和修改,但是不方便新增和删除

    2)链表适合新增和删除,但是不适合查询,根据业务情况使用合适的数据结构和算法是在大数据量和高并发时必须要考虑的问题

    栈和队列的应用

    是一种可以实现“先进后出”的存储结构

    类似于一个箱子,先放进去的书,最后才能取出来,后放进去的书,先去出来。因为在栈中永远只能先操作栈顶的元素。对栈顶的元素进行push和pop。

    函数调用在栈中的应用

    函数在调用的时候会在内存中开辟一块栈空间,在调用一个函数的时候,会将这个函数压入我们申请的栈内,如果此时调用的还有其他函数,按照“先调用后返回”的原则,先入栈(push)的这个函数会将函数执行的控制权交给后入栈(push)的函数,依次类推,将这些函数压入栈中,直到将执行函数的控制权交给栈顶的那个函数。栈顶的那个函数执行完之后,会出栈(pop),然后将函数的控制权交给当前栈顶的元素(函数),依次出栈,直到程序判断栈空,所有代码执行完毕。

    使用python已有的数据结构,简单实现一个栈结构

    # 栈: 先进后出
    l =[1,2,3,4,5]
    l.append(6)
    l.append(7)
    while True:
        try:
            res = l.pop()
            print(res)
        except:
            print('列表已经空了!')
            break
    
    

    队列

    是一种可以实现“先进先出”的存储结构。

    类似于一个管道,先入管道的,先从管道出来。

    应用:生产者消费者模型(起源于操作系统)
    使用python已有的数据结构,简单实现一个队列结构

    # 队列:先进先出
    from collections import deque
    queue = deque(l)
    queue.append(4)
    queue.append(5)
    print(queue)  # deque([1, 2, 3, 4, 5])
    print(queue.popleft())  # 1
    print(queue.popleft())  # 2
    print(queue)  # deque([3, 4, 5])
    

    python实现二叉树的前、中、后序遍历

    我们先创建一颗二叉树:

    class BinaryTree:
        def __init__(self, data):
            self.data = data
            self.left = None
            self.right = None
    
        def get(self):
            return self.data
    
        def getLeft(self):
            return self.left
    
        def getRight(self):
            return self.right
    
        def setLeft(self, node):
            self.left = node
    
        def setRight(self, node):
            self.right = node
    

    好,这里我们定义好了一个二叉树类,并给它添加了一下方法,然后我们来实例化一颗二叉树:

    binaryTree = BinaryTree(0)
    binaryTree.setLeft(BinaryTree(1))
    binaryTree.setRight(BinaryTree(2))
    binaryTree.getLeft().setLeft(BinaryTree(3))
    binaryTree.getLeft().setRight(BinaryTree(4))
    binaryTree.getRight().setLeft(BinaryTree(5))
    binaryTree.getRight().setRight(BinaryTree(6))
    

    实例化好的二叉树是长这个样子的:

    前序遍历

    前序遍历的顺序就是先遍历树的父节点,然后遍历树的左节点,然后遍历树的右节点,以此类推。
    对于我们上面定义好的二叉树来说,它的前序遍历结果就是:0 -> 1 -> 3 -> 4 -> 2 -> 5 -> 6
    对于前序、中序、后序遍历来说,采用递归的方式是非常方便的。这里我们就用递归来实现一下

    def preorderTraversal(now, result=[]):
        if now == None:
            return result
        result.append(now.data)
        preorderTraversal(now.left, result)
        preorderTraversal(now.right, result)
        return result
    
    print(preorderTraversal(binaryTree))
    
    # 执行结果:[0, 1, 3, 4, 2, 5, 6],是不是和我们之前手动遍历的结果一样呢。
    

    中序遍历

    中序遍历的顺序是:先遍历树的左节点,再遍历树的父节点,再遍历树的右节点。
    对于我们上面创建的二叉树,它的中序遍历结果就是:3 -> 1 -> 4 -> 0 -> 5 -> 2 -> 6
    在前序遍历的时候是先遍历父节点,所以result.append(now.data),就在遍历左节点和右节点的前面。
    而中序遍历要先遍历左节点,所以result.append(now.data)就要在遍历左节点的后面,遍历右节点的前面。

    def intermediateTraversal(now, result=[]):
        if now == None:
            return result
        intermediateTraversal(now.left, result)
        result.append(now.data)
        intermediateTraversal(now.right, result)
        return result
    
    print(intermediateTraversal(binaryTree))
    # 执行结果:[3, 1, 4, 0, 5, 2, 6]
    

    后序遍历

    后序遍历顺序是:先遍历树的左节点,再遍历树的右节点,再遍历树的父节点。
    对于我们上面创建的二叉树,它的后序遍历结果是:3 -> 4 -> 1 -> 5 -> 6 -> 2 -> 0
    相应的递归方程为:

    def postorderTraversal(now, result=[]):
        if now == None:
            return
        postorderTraversal(now.left, result)
        postorderTraversal(now.right, result)
        result.append(now.data)
        return result
    
    print(postorderTraversal(binaryTree))
    # 执行结果:[3, 4, 1, 5, 6, 2, 0]
    
  • 相关阅读:
    C语言 · 阶乘计算 · 基础练习
    C语言 · 查找整数 · 基础练习
    UML课程复习重点
    运维参考
    mysql语法总结
    Python杂篇
    Python练习题
    Python参考
    k8s中ipvs和iptables选择
    安装cni网络插件-非必须
  • 原文地址:https://www.cnblogs.com/zhangchaocoming/p/12670550.html
Copyright © 2020-2023  润新知