数据结构与算法|数据结构与算法


据结构和算法 2017-04-05

  1. 数据结构和算法概述
    1. 数据结构的定义
    2. 数据结构的特点
    3. 算法
      1. 衡量算法的标准
  2. 线性结构
    1. 数组和链表的区别
    2. 连续存储(数组)
    3. 离散存储(链表)
    4. 数组和链表的性能比较
    5. 线性结构的两种应用方式之栈
      1. 栈的定义
      2. 栈的分类
      3. 栈的算法
      4. 栈的应用
    6. 线性结构的两种应用方式之队列
      1. 队列的定义
      2. 队列的分类
      3. 链式队列的算法
      4. 队列的实际应用
    7. 递归
      1. 定义
      2. 多个函数调用
      3. 递归满足的条件
      4. 求阶乘
      5. 1+2+3…+100的和
      6. 递归和循环的区别
      7. 递归的应用
  3. 阶段总结
  4. 非线性结构
      1. 树的定义
      2. 树的专业术语
      3. 树的分类
      4. 树的操作(伪算法)
      5. 二叉树具体的操作
      6. 树的应用
  5. 总结
  6. 推荐的学习书籍
数据结构和算法概述 数据结构的定义
我们如何把现实中大量而且非常复杂的问题以特定的数据类型(个体)和特定的存储结构(个体的关系)保存到相应的主存储器(内存)中,以及在此基础上为实现某个功能而执行的相应操作,这个相应的操作也叫做算法
数据结构 == 个体 + 个体的关系
算法 == 对存储数据的操作
数据结构的特点
【数据结构与算法|数据结构与算法】数据结构是软件中最核心的课程
程序 = 数据的存储 + 数据的操作 + 可以被计算机执行的语言
算法 衡量算法的标准
  • 时间复杂度 指的是大概程序执行的次数,而非程序执行的时间
  • 空间复杂度 指的是程序执行过程中,大概所占有的最大内存
  • 难易程度
  • 健壮性
常见的几个排序:
  • 冒泡排序
  • 选择排序
  • 插入排序
  • 快速排序
  • 归并排序
  • 计数排序
常见的查找:
  • 顺序查找
  • 二分查找
应用场景:
  • 各种榜单
  • 各种表格
  • 给二分查找用
  • 给其他算法用
timeit的使用:
点击显/隐
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 from timeit import Timer def t1(): li = [] for i in range( 10000): li.append(i) def t2(): li = [] for i in range( 10000): li = li + [i] def t3(): li = [ i for i in range( 10000)] def t4(): li = list(range( 10000)) def t5(): li = [] for i in range( 10000): li.extend([i]) tm1 = Timer( "t1()", "from __main__ import t1") print( "append:", tm1.timeit( 1000)) tm2 = Timer( "t2()", "from __main__ import t2") print( "+:", tm2.timeit( 1000)) tm3 = Timer( "t3()", "from __main__ import t3") print( "[i for i in range]:", tm3.timeit( 1000)) tm4 = Timer( "t4()", "from __main__ import t4") print( "list:", tm4.timeit( 1000)) tm5 = Timer( "t5()", "from __main__ import t5") print( "extend:", tm5.timeit( 1000)) ### 测试往队头和队尾进行添加 def t6(): li = [] for i in range( 10000): li.append(i) def t7(): li = [] for i in rnage( 10000): li.insert( 0,i) tm6 = Timer( "t6()", "from __main__ import t6") print( "append:", tm6.timeit( 1000)) tm7 = Timer( "t7()", "from __main__ import t7") print( "insert:", tm7.timeit( 1000))
算法的思想:
  • 穷举法 brute force
  • 分治法 divide conquer
  • 模拟
  • 递归
  • 贪心
  • 动态规划
线性结构
把所有的节点用一根线串起来
数组和链表的区别
数组需要一块连续的内存空间来存储,对内存的要求比较高。如果我们申请一个 100MB 大小的数组,当内存中没有连续的、足够大的存储空间时,即便内存的剩余总可用空间大于 100MB,仍然会申请失败。 而链表恰恰相反,它并不需要一块连续的内存空间,它通过“指针”将一组零散的内存块串联起来使用,所以如果我们申请的是 100MB 大小的链表,根本不会有问题。
数据结构与算法|数据结构与算法
文章图片

连续存储(数组)
数组,在其python语言中称为列表,是一种基本的数据结构类型
关于列表的问题:
  • 数组(列表)中的元素是如何存储的
  • 列表提供了哪些最基本的操作?
  • 这些操作的时间复杂度是多少?
  • 为啥数组(列表)的索引或者下标是从0开始?
关于数组(列表)的优缺点
  • 优点:
    • 存取速度快
  • 缺点:
    • 事先需要知道数组的长度
    • 需要大块的连续内存
    • 插入删除非常的慢,效率极低
离散存储(链表) 1.定义:
  • n个节点离散分配
  • 彼此通过指针相连
  • 每个节点只有一个前驱节点,每个节点只有一个后续节点
  • 首节点没有前驱节点,尾节点没有后续节点
优点:
  • 空间没有限制,插入删除元素很快
缺点:
  • 查询比较慢
链表的节点的结构如下:
指针域
data next
数据结构与算法|数据结构与算法
文章图片

data为自定义的数据,next为下一个节点的地址。
2.专业术语:
  • 首节点:第一个有效节点
  • 尾节点:最后一个有效节点
  • 头结点:第一个有效节点之前的那个节点,头结点并不存储任何数据,目的是为了方便对链表的操作
  • 头指针:指向头结点的指针变量
  • 尾指针:指向尾节点的指针变量
3.链表的分类:
  • 单链表
  • 双链表 每一个节点有两个指针域
  • 循环链表 能通过任何一个节点找到其他所有的节点
  • 非循环链表
4.算法:
  • 增加
  • 删除
  • 修改
  • 查找
  • 总长度
有一堆数据1,2,3,5,6,7我们要在3和5之间插入4,如果用数组,我们会怎么做?当然是将5之后的数据往后退一位,然后再插入4,这样非常麻烦,但是如果用链表,我就直接在3和5之间插入4就行,听着就很方便
5.如果希望通过一个函数来对链表进行处理操作,至少需要接受链表的哪些参数?
只需要一个参数,头结点即可,因为我们可以通过头结点来推算出链表的其他所有的参数
数据结构与算法|数据结构与算法
文章图片

6.单链表的算法
点击显/隐
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 class Hero(object): def __init__(self, no=0, name="", nickname="", pNext=None): self.no = no self.name = name self.nickname = nickname self.pNext = pNext def getHero(head, no): cur = head while cur.pNext != None: if cur.no == no: print( "找到的英雄的编号是: %s,姓名是:%s,外号是:%s" % (cur.no, cur.name, cur.nickname)) break cur = cur.pNext else: print( "没有这个英雄") def add(head, hero): # 1. 直接在链表的最后加上 # 先找到链表的最后 cur = head # while cur.pNext != None: # cur = cur.pNext # # 当退出链表的时候,就算是队尾了 # cur.pNext = hero # 2. 指定位置进行添加 while cur.pNext != None: if cur.pNext.no >= hero.no: # 找到位置 break # 继续往下走 cur = cur.pNext hero.pNext = cur.pNext cur.pNext = hero def showAll(head): cur = head while cur.pNext != None: print( "英雄的编号是: %s,姓名是:%s,外号是:%s" % (cur.pNext.no,cur.pNext.name,cur.pNext.nickname)) cur = cur.pNext def delHero(head, no): cur = head while cur.pNext != None: if cur.pNext.no == no: # 开始删除 cur.pNext = cur.pNext.pNext break cur = cur.pNext else: print( '没有找到') def updateHero(head, no, name): cur = head while cur.pNext != None: if cur.pNext.no == no: cur.pNext.name = name break cur = cur.pNext else: print( '没有找到英雄') def is_empty(head): if head.pNext == None: return True return False def length(head): cnt = 0 cur = head while cur.pNext != None: cnt = cnt + 1 cur = cur.pNext return cnt # 创建一个head头,该head只是一个头,不存放数据 head = Hero() # print(is_empty(head)) hero = Hero( 1, '宋江', '及时雨') # head.pNext = hero add(head, hero) hero = Hero( 2, '卢俊义', '玉麒麟') # hero.pNext = hero2 add(head, hero) hero = Hero( 6, '林冲', '豹子头') add(head, hero) hero = Hero( 3, '吴用', '智多星') add(head, hero) # 遍历所有的英雄 showAll(head) # delHero(head, 3) # print('删除之后的排列:') updateHero(head, 3, '张三') print( '###############修改之后的排列:################') showAll(head) print(length(head)) # getHero(head, 3)
7.双向链表
双链表中每个节点有两个指针:一个指向后面节点、一个指向前面节点
数据结构与算法|数据结构与算法
文章图片

1 2 3 4 5 class Node(object): def __init__(self, data=https://www.it610.com/article/None): self.data = data self.next = None self.prior = None
双向链表的操作:
插入:
1 2 3 4 p.next = curNode.next curNode.next.prior = p curNode.next = p p.prior = curNode
删除:
1 2 3 4 p = curNode.next curNode.next = p.next p.next.prior = curNode del p
数据结构与算法|数据结构与算法
文章图片

8.循环链表
循环链表是另一种形式的链式存贮结构。它的特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环。
9.约瑟夫问题
设编号为1,2,… n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m 的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列
点击显/隐
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 # 循环链表 class Child(object): first = None def __init__(self, no = None, pNext = None): self.no = no self.pNext = pNext def addChild(self, n=4): cur = None for i in range(n): child = Child(i + 1) if i == 0: self.first = child self.first.pNext = child cur = self.first else: cur.pNext = child child.pNext = self.first cur = cur.pNext def showChild(self): cur = self.first while cur.pNext != self.first: print( "小孩的编号是:%d" % cur.no) cur = cur.pNext print( "小孩的编号是: %d" % cur.no) def countChild(self, m, k): tail = self.first while tail.pNext != self.first: tail = tail.pNext # 出来后,已经是在first前面 # 从第几个人开始数 for i in range(k -1): tail = tail.pNext self.first = self.first.pNext # 数两下,就是让first和tail移动一次 # 数三下,就是让first和tail移动两次 while tail != self.first: # 当tail == first 说明只剩一个人 for i in range(m -1): tail = tail.pNext self.first = self.first.pNext self.first = self.first.pNext tail.pNext = self.first print( "最后留在圈圈中的人是:%d" % tail.no) c = Child() c.addChild( 4) c.showChild() c.countChild( 3, 2)
数组和链表的性能比较 数据结构与算法|数据结构与算法
文章图片

线性结构的两种应用方式之栈 栈的定义
一种可以实现“先进后出”的存储结构
栈类似于一个箱子,先放进去的书,最后才能取出来,同理,后放进去的书,先取出来
数据结构与算法|数据结构与算法
文章图片

栈的分类
  • 静态栈
    • 静态栈的核心是数组,类似于一个连续内存的数组,我们只能操作其栈顶元素
  • 动态栈
    • 动态栈的核心是链表
数据结构与算法|数据结构与算法
文章图片

栈的算法 栈的算法主要是压栈和出栈两种操作的算法,下面我就用代码来实现一个简单的栈。
首先要明白以下思路:
  • 栈操作的是一个一个节点
  • 栈本身也是一种存储的数据结构
  • 栈有初始化压栈出栈判空遍历清空等主要方法
点击显/隐
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 class Stack(object): def __init__(self): self.pTop = None self.pBottom = None class Node(object): def __init__(self, data=https://www.it610.com/article/None, pNext = None): self.data = data self.pNext = pNext def push(s, new): new.pNext = s.pTop s.pTop = new def pop(s): cur = s.pTop # while cur != s.pBottom: # if cur.data == val: # s.pTop = cur.pNext # break # cur = cur.pNext # else: # print('没有找到此元素') while cur != s.pBottom: s.pTop = cur.pNext print( "出栈的元素是: %d" % cur.data) cur = cur.pNext else: print( "出栈失败") def getAll(s): cur = s.pTop while cur != s.pBottom: print(cur.data) cur = cur.pNext def is_empty(s): if s.pTop == s.pBottom: return True else: return False def clear(s): if is_empty(s): return None p = s.pTop q = None while p != s.pBottom: q = p.pNext del p p = q else: s.pBottom = s.pTop head = Node() s = Stack() s.pTop = s.pBottom = head n1 = Node( 2) push(s, n1) n1 = Node( 5) push(s, n1) n1 = Node( 89) push(s, n1) print( "##############遍历元素##########") getAll(s) # print("##############出栈元素#######") # pop(s) print( "##############清空栈##########") clear(s) print( "##############遍历元素##########") getAll(s)
栈的应用
  • 函数调用
  • 浏览器的前进或者后退
  • 表达式求值
  • 内存分配
…..
线性结构的两种应用方式之队列 队列的定义
一种可以实现“先进先出”的数据结构
队列的分类
  • 链式队列
  • 静态队列
链式队列的算法 点击显/隐
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 class Node: def __init__(self, value): self.data = https://www.it610.com/article/value self.next = None class Queue: def __init__(self): self.front = Node( None) self.rear = self.front def enQueue(self, element): n = Node(element) self.rear.next = n self.rear = n def deQueue(self): if self.empty(): print('队空') return temp = self.front.next self.front = self.front.next if self.rear == temp: self.rear = self.front del temp def getHead(self): if self.empty(): print( '队空') return return self.front.next.data def empty(self): return self.rear == self.front def printQueue(self): cur = self.front.next while cur != None: print(cur.data) cur = cur.next def length(self): cur = self.front.next count = 0 while cur != None: count += 1 cur = cur.next return count if __name__ == '__main__': queue = Queue() queue.enQueue( 23) queue.enQueue( 2) queue.enQueue( 4) queue.printQueue() queue.deQueue() # queue.printQueue() l = queue.length() print( "长度是: %d" % l) queue.deQueue() # queue.printQueue() l = queue.length() print( "长度是: %d" % l) queue.deQueue() # queue.printQueue() l = queue.length() print( "长度是: %d" % l)
队列的实际应用
所有和时间有关的操作都和队列有关
递归 定义
一个函数自己或者间接调用自己
多个函数调用
当有多个函数调用时,按照“先调用后返回”的原则,函数之间的信息传递和控制转移必须借助栈来实现,即系统将整个程序运行时所需要的数据空间安排在一个栈中,每当调用一个函数时,就在栈顶分配一个存储区,进行压栈操作,每当一个函数退出时,就释放他的存储区,即进行出栈操作,当前运行的函数永远在栈顶的位置
1 2 3 4 5 6 7 8 9 10 11 12 13 def f(): print( 'FFFFFFF') g() def g(): print( 'GGGGGGG') k() def k(): print( 'KKKKKKK') if __name__ == "__main__": f()
自己调用自己也是和上面的原理一样
1 2 3 4 5 def f(n): if n == 1: print( 'hello') else: f(n -1)
递归满足的条件
  • 递归必须有一个明确的终止条件
  • 该函数所处理的数据规模是必须递减的
  • 这个转化必须是可解的
求阶乘
n规模的实现,得益于n-1规模的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 # for循环实现 multi = 1 for i in range( 3): multi = multi * (i+ 1) print(multi) # 递归实现 def f(n): if 1 == n: return 1 else: return f(n -1)*n
1+2+3…+100的和
1 2 3 4 5 def sum(n): if 1 == n: return n else: return sum(n -1) + n
递归和循环的区别
  • 递归
    • 不易于理解
    • 速度慢
    • 存储空间大
  • 循环
    • 易于理解
    • 速度快
    • 存储空间小
递归的应用 树和森林就是以递归的方式定义的
树和图的算范就是以递归的方式实现的
很多数学公式就是以递归的方式实现的(斐波那楔序列)
阶段总结 数据结构:
从狭义的方面讲:
  • 数据结构就是为了研究数据的存储问题
  • 数据结构的存储包含两个方面:个体的存储 + 个体关系的存储
从广义方面来讲:
  • 数据结构既包含对数据的存储,也包含对数据的操作
  • 对存储数据的操作就是算法
算法
从狭义的方面讲:
  • 算法是和数据的存储方式有关的
从广义的方面讲:
  • 算法是和数据的存储方式无关,这就是泛型的思想
非线性结构 树 树的定义 我们可以简单的认为:
  • 树有且仅有一个根节点
  • 有若干个互不相交的子树,这些子树本身也是一颗树
通俗的定义:
1.树就是由节点和边组成的
2.每一个节点只能有一个父节点,但可以有多个子节点。但有一个节点例外,该节点没有父节点,此节点就称为根节点
树的专业术语
  • 节点
  • 父节点
  • 子节点
  • 子孙
  • 堂兄弟
  • 兄弟
  • 深度
    • 从根节点到最底层节点的层数被称为深度,根节点是第一层
  • 叶子节点
    • 没有子节点的节点
    • 子节点的个数
树的分类
  • 一般树
    • 任意一个节点的子节点的个数不受限制
  • 二叉树
    • 定义:任意一个节点的子节点的个数最多是两个,且子节点的位置不可更改
      • 满二叉树
        • 定义:在不增加层数的前提下,无法再多添加一个节点的二叉树
      • 完全二叉树
        • 定义:只是删除了满二叉树最底层最右边连续的若干个节点
      • 一般二叉树
  • 森林
    • n个互不相交的数的集合
数据结构与算法|数据结构与算法
文章图片

树的操作(伪算法) 如何把一个非线性结构的数据转换成一个线性结构的数据存储起来?
  • 一般树的存储
    • 双亲表示法
      • 求父节点方便
    • 孩子表示法
      • 求子节点方便
    • 双亲孩子表示法
      • 求父节点和子节点都很方便
    • 二叉树表示法
      • 即把一般树转换成二叉树,按照二叉树的方式进行存储
      • 具体的转化办法:
        • 设法保证任意一个节点的:
          • 左指针域指向它的第一个孩子
          • 右指针域指向它的下一个兄弟
        • 只要能满足上述的条件就能够转化成功
  • 二叉树的操作
    • 连续存储 (完全二叉树,数组方式进行存储)
      • 优点:查找某个节点的父节点和子节点非常的快
      • 缺点:耗用内存空间过大
      • 转化的方法:先序 中序 后序
    • 链式存储 (链表存储)
      • data区域 左孩子区域 右孩子区域
  • 森林的操作
    • 把所有的树转化成二叉树,方法同一般树的转化
二叉树具体的操作 1.二叉树的先序遍历[先访问根节点]
  • 先访问根节点
  • 再先序遍历左子树
  • 再先序遍历右子树
2.二叉树的中序遍历 [中间访问根节点]
  • 先中序遍历左子树
  • 再访问根节点
  • 再中序遍历右子树
3.二叉树的后序遍历 [最后访问根节点]
  • 先中序遍历左子树
  • 再中序遍历右子树
  • 再访问根节点
4.已知先序和中序,如何求出后序?
1 2 3 4 5 6 7 8 9 10 11 12 13 #### 例一 先序:ABCDEFGH 中序:BDCEAFHG 求后序? 后序:DECBHGFA #### 例二 先序:ABDGHCEFI 中序:GDHBAECIF 求后序? 后序:GHDBEIFCA
5.已知中序和后序,如何求出先序?
1 2 3 4 5 中序:BDCEAFHG 后序:DECBHGFA 求先序? 先序:ABCDEFGH
点击显/隐
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 class Node(object): """节点类""" def __init__(self, elem=-1, lchild=None, rchild=None): self.elem = elem self.lchild = lchild self.rchild = rchild class Tree(object): """树类""" def __init__(self): self.root = Node() self.myli = [] def add(self, elem): """为树添加节点""" node = Node(elem) if self.root.elem == -1: # 如果树是空的,则对根节点赋值 self.root = node self.myli.append(self.root) else: treeNode = self.myli[ 0] # 此结点的子树还没有齐。 if treeNode.lchild == None: treeNode.lchild = node self.myli.append(treeNode.lchild) else: treeNode.rchild = node self.myli.append(treeNode.rchild) self.myli.pop( 0) def front_digui(self, root): """利用递归实现树的先序遍历""" if root == None: return print(root.elem) self.front_digui(root.lchild) self.front_digui(root.rchild) def middle_digui(self, root): """利用递归实现树的中序遍历""" if root == None: return self.middle_digui(root.lchild) print(root.elem) self.middle_digui(root.rchild) def later_digui(self, root): """利用递归实现树的后序遍历""" if root == None: return self.later_digui(root.lchild) self.later_digui(root.rchild) print(root.elem) if __name__ == '__main__': """主函数""" elems = range( 10) #生成十个数据作为树节点 tree = Tree() #新建一个树对象 for elem in elems: tree.add(elem) #逐个添加树的节点 print( '递归实现先序遍历:') tree.front_digui(tree.root)
树的应用
  • 树是数据库中数据组织的一种重要形式
  • 操作系统子父进程的关系本身就是一颗树
  • 面型对象语言中类的继承关系
总结
数据结构研究的就是数据的存储和数据的操作的一门学问
数据的存储又分为两个部分:
  • 个体的存储
  • 个体关系的存储
从某个角度而言,数据存储最核心的就是个体关系如何进行存储
个体的存储可以忽略不计
推荐的学习书籍 数据结构与算法|数据结构与算法
文章图片

转载于:https://www.cnblogs.com/guanchao/p/11439174.html

    推荐阅读