• 数据结构与算法之美


    1. 什么是数据结构?什么是算法
      • 广义
        • 数据结构:一组数据的存储结构
        • 算法:操作数据的一组方法
        • 例子:图书馆对书籍的分门别类存储=数据结构,查找一本书的方法=算法
      • 狭义
        • 著名的数据结构和算法:队列,栈,堆,二分查找,动态规划 
      • 重点:复杂度分析
        • 10个常用的数据结构:数组、链表、栈、队列、散列表、二叉树、堆、跳表、图、Trie树 
        • 10个常用的算法:递归、排序、二分查找、搜索、哈希算法、贪心算法、分治算法、回溯算法、动态规划、字符串匹配算法 
      • 方法:边学边练,适度刷题
        • 打怪升级学习法:设定目标,达到目标
        • 沉淀法:书读百遍其义自见 
    2. 复杂度分析
      • 数据结构和算法本身解决的问题是"快"和“省”的问题
      • 为什么需要复杂度分析?
        • 测试结果非常依赖测试环境
        • 测试结果受数据规模的影响很大
      • 时间复杂度
        • 大O复杂度表示法:所有代码的执行时间与每行代码的执行次数成正比
          • 只关注循环次数最多的一段代码
          • 加法法则:总复杂度等于量级最大的那段代码的复杂度
          • 乘法法则:嵌套代码复杂度等于嵌套内外代码复杂度的乘积
        • 常见时间复杂度
          • 多项式量级
            • 常量阶O(1):代码的执行时间不随n的增大而增长
            • 对数阶O(logn):i=1;while (i <= n) {i = i * 2;}----计算循环执行次数最多的行的时间复杂度
            • 线性对数阶O(nlogn):循环N次的对数阶 
            • 线性阶O(n):循环N次
            • 平方阶O(n2) 立方阶O(n3),K次方阶O(nk)  
          • 非多项式量级:非常低效的算法
            • 指数阶O(2n)
            • 阶乘阶O(n!)     
      • 空间复杂度:渐进空间复杂度,表示算法的存储空间和数据规模的增长关系
        • 同时间复杂度,只需要算量级最大的空间存储  
      • 最好情况时间复杂度:理想情况的时间复杂度
      • 最坏情况时间复杂度:最糟糕的情况下的时间复杂度
      • 平均情况时间复杂度:所有出现的情况累加除以可能性出现的情况,加权平均复杂度,期望时间复杂度
      • 均摊时间复杂度:把最坏的情况的时间复杂度均摊到所有耗时少的情况下---使用很少        
    3. 数组
      • 数组为什么从0开始编号?
        • 下标应该定义为偏移,下标其实是为了确定与首地址的偏移位置
        • C语言一开始用,后面的语言继承了这一点   
      • 线性表:数据排成像一条线一样的结构
      • 连续的内存空间和相同数据类型:可以通过地址随机访问数组中的某个元素
      • JVM清楚垃圾回收:标记已经删除的位置,当没有更多的存储空间的时候在执行删除操作
        • 垃圾桶。生活中,我们扔进屋里垃圾桶的垃圾,并没有消失,只是被 '' 标记 '' 成了垃圾,只有垃圾桶塞满时,才会清理垃圾桶。再次存放垃圾
    4. 链表
      • 链表和数组的区别
        • 数组需要连续的内存空间,链表不需要连续的存储空间
        • 插入删除:数组O(n),链表O(1)
        • 随机访问:数组O(1),链表O(n)
        • 数组利于CPU的缓存机制,但是固定内存空间
        • 链表可以动态扩容,但是没法有效的预读,对CPU的缓存不友好
      • 单链表
        • 头节点:第一个节点
        • 尾节点:最后一个节点,指向Null
        • 插入和删除,不需要为了保证连续而搬移节点,O(1)
        • 随机查找,没有连续的存储,没有公式,只能一个一个往下数,O(n)
        • 每个节点:值,后继指针
      • 循环链表
        • 特殊的单链表,唯一的区别是尾节点指向头节点  
      • 双向链表
        • 每个节点:前驱指针,值,后继指针 
        • 删除操作
          • 删除值等于某个定值的节点:需要遍历查找O(n)
          • 删除给定指针指向的节点:需要删除前驱节点,还是得遍历O(n)
        • 插入操作也同理
        • 随机查找也比单链表更加高效  
        • 空间换时间的设计思想:缓存  
      • 代码的书写
        • 技巧一:理解指针或引用的含义
          • 将某个变量赋值给指针,实际上这个变量的地址赋值给指针  
        • 技巧二:警惕指针丢失和内存泄露
        • 技巧三:利用哨兵简化实现难度
          • 哨兵:head指针一直指向哨兵节点,哨兵节点不存储数据 
        • 技巧四:重点留意边界条件的处理
          • 如果链表为空时,代码是否能正常工作?
          • 如果链表只包含一个结点时,代码是否能正常工作?
          • 如果链表只包含两个结点时,代码是否能正常工作?
          • 代码逻辑在处理头结点和尾结点的时候,是否能正常工作?
        • 技巧五:举例画图,辅助思考
        • 技巧六:多写多练,没有捷径
        • 常见的操作
          • 单链表反转
          • 链表中环的检测
          • 两个有序的链表合并
          • 删除链表倒数第n个结点
          • 求链表的中间结点
      • 如何实现浏览器的前进和后退功能?
        • 我们使用两个栈, X 和 Y ,我们把首次浏览的页面依次压入栈 X ,当点击后退按钮时,再依次从栈 X 中出栈,并将出栈的数据依次放入栈 Y 。当我们点击前进按钮时,我们依次从栈 Y 中取出数据,放入栈 X 中。当栈 X 中没有数据时,那就说明没有页面可以继续后退浏览了。当栈 Y 中没有数据,那就说明没有页面可以点击前进按钮浏览了。
      • 理解:一摞叠在一起的盘子,只能后进先出,先进后出
      • 操作
        • 只能在一端插入和删除-----入栈和出栈 O(1)
      • 栈在函数调用中的应用
        • 操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成 “ 栈 ” 这种结构 , 用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈
      • 栈在表达式求值中的应用
    5. 队列
      • 队列在线程池等有限资源池中的应用
        • 第一种是非阻塞的处理方式,直接拒绝任务请求;
        • 另一种是阻塞的处理方式,将请求排队,等到有空闲线程时,取出排队的请求继续处理。
      • 理解:排队买票,先进先出
      • 操作:只支持入队,出队  
        • 我们在出队时可以不用搬移数据。如果没有空闲空间了,我们只需要在入队时,再集中触发一次数据的搬移操作。
      • 循环队列
        • 循环队列会浪费一个数组的存储空间
      • 阻塞队列
        • 就是在队列为空的时候,从队头取数据会被阻塞。因为此时还没有数据可取,直到队列中有了数据才能返回;如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回。
      • 并发队列
        • 最简单直接的实现方式是直接在enqueue()、dequeue()方法上加锁,但是锁粒度大并发度会比较低,同一时刻仅允许一个存或者取操作  
    6. 递归
      • 如何找到最终推荐人?
      • 如何理解:
        • .一个问题的解可以分解为几个子问题的解
        • 这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样
        • 存在递归终止条件
      • 编写递归代码的关键是,只要遇到递归,我们就把它抽象成一个递推公式,不用想一层层的调用关系,不要试图用人脑去分解递归的每个步骤
      • 递归代码限制最大深度,避免堆栈溢出 
      • 优点:写起来简洁
      • 缺点:空间复杂度高,有堆栈溢出风险、存在重复计算、过多的函数调用会耗时较多等问题。
    7. 排序
      • 为什么插入排序比冒泡排序更受欢迎?
        •     
      • 基于比较的排序算法。
        • 冒泡排序
          • 操作相邻的两个数据
          • '''
                冒泡排序:比较相邻元素,顺序错误就交换顺序
             
            '''
            def bubble_Sort(nums):
                for i in range(len(nums)-1):
                    for j in range(len(nums)-1-i):
                        if nums[j] > nums[j+1]:
                            nums[j],nums[j+1] = nums[j+1],nums[j]
                return nums  
          • 冒泡排序是原地排序算法吗?
            • 冒泡的过程只涉及相邻数据的交换操作,只需要常量级的临时空间,所以它的空间复杂度为 O(1) ,是一个原地排序算法
          • 冒泡排序是稳定的排序算法吗?
            • 在冒泡排序中,只有交换才可以改变两个元素的前后顺序。为了保证冒泡排序算法的稳定性,当有相邻的两个元素大小相等的时候,我们不做交换,相同大小的
              数据在排序前后不会改变顺序,所以冒泡排序是稳定的排序算法。

          • 冒泡排序的时间复杂度是多少?

            • 最好情况下,要排序的数据已经是有序的了,我们只需要进行一次冒泡操作,就可以结束了,所以最好情况时间复杂度是 O(n) 。而最坏的情况是,要排序的数据
              刚好是倒序排列的,我们需要进行n次冒泡操作,所以最坏情况时间复杂度为O(n 2 )。

        • 插入排序---这个用的还是挺多的

          • '''
                插入排序:假设元素左侧全部有序,找到自己的位置插入
            '''
            def insert_sort(nums):
                for i in range(1,len(nums)):
                    for j in range(i,0,-1):
                        if nums[j-1] <= nums[j]:
                            break
                        elif nums[j-1] > nums[j]:
                            nums[j-1],nums[j] = nums[j],nums[j-1]
             
                return nums 
          • 冒泡排序是原地排序算法吗?

            • 从实现过程可以很明显地看出,插入排序算法的运行并不需要额外的存储空间,所以空间复杂度是 O(1) ,也就是说,这是一个原地排序算法。

          • 插入排序的时间复杂度是多少?

            • 如果要排序的数据已经是有序的,我们并不需要搬移任何数据。如果我们从尾到头在有序数据组里面查找插入位置,每次只需要比较一个数据就能确定插入的位
              置。所以这种情况下,最好是时间复杂度为O(n)。注意,这里是从尾到头遍历已经有序的数据。
              如果数组是倒序的,每次插入都相当于在数组的第一个位置插入新的数据,所以需要移动大量的数据,所以最坏情况时间复杂度为O(n 2 )。平均时间复杂度为O(n 2 )

        • 选择排序

          • '''
                选择排序:选择最小的,以此类推
            '''
            def select_Sort(nums):
                for i in range(len(nums)-1):
                    for j in range(i+1,len(nums)):
                        if nums[i] > nums[j]:
                            nums[i],nums[j] = nums[j],nums[i]
                return nums
            
          • 排序是稳定的排序算法吗?  

            • 选择排序每次都要找剩余未排序元素中的最小值,并和前面的元素,交换位置,这样破坏了稳定性

        • 快速排序
          • '''
                快速排序:分而治之,一分为二进行排序
            '''
            def quick_sort(nums):
                if len(nums) <= 1:
                    return nums
                s_nums = []
                l_nums = []
                #小于nums[0]放左边
                for i in nums[1:]:
                    if i < nums[0]:
                        s_nums.append(i)
                    else:
                    # #大于nums[0]放右边
                        l_nums.append(i)
                #nums[0:1]是列表[],nums[0]是int数字
                #连接左右列表加num[0:1]
                return quick_sort(s_nums)+nums[0:1]+quick_sort(l_nums)
            
          • 大部分情况下的时间复杂度都可以做到O(nlogn),只有在极端情况下,才会退化到O(n 2 )      
        • O(n)时间复杂度内求无序数组中的第K大元素?
          •     

       

    '''
        快速排序:分而治之,一分为二进行排序
    '''
    import cProfile
    import random
    def quick_sort(nums):
        if len(nums) <= 1:
            return nums
        s_nums = []
        l_nums = []
        #小于nums[0]放左边
        for in nums[1:]:
            if i < nums[0]:
                s_nums.append(i)
            else:
            # #大于nums[0]放右边
                l_nums.append(i)
        #nums[0:1]是列表[],nums[0]是int数字
        #连接左右列表加num[0:1]
        return quick_sort(s_nums)+nums[0:1]+quick_sort(l_nums)
  • 相关阅读:
    How to use VS2012 remote debug Windows Azure Cloud Services
    vue ---05 分页和详情页功能的实现
    vue ----04课程列表的展示
    vue--03 首页和登陆注册
    luffy--03 首页和登陆注册(跨域问题的解决)
    luffy--02 ---项目配置和数据库链接
    luffy---01
    DRF---一些配置/设置
    drf-路由
    drf视图
  • 原文地址:https://www.cnblogs.com/shuimohei/p/12590093.html
Copyright © 2020-2023  润新知