数据结构与算法之线性结构和树结构

什么是数据结构

数据结构是指相互之间存在着一种或多种关系的数据元素的集合和该集合中数据元素之间的关系的组成。python

  • 数据结构就是设计数据以何种方式存储在计算机中,列表、字典等都算是数据结构。linux

  • 程序=数据结构+算法,数据结构属于静态的部分,算法的调用为动态部分算法

数据结构的分类

根据逻辑结构划分:数据库

  • 线性结构:数据结构中的元素一对一的关系,一前驱,一后继。
  • 树结构:数据结构中元素一对多的关系,一前驱,多后继。
  • 图结构:数据结构中元素存在多对多的关系,多前驱,多后继,我也不会。
    • 判断一个图形能不能一笔画完,就判断它的奇数度节点数目是否为0或2.这种能一笔画完的就是欧拉图,奇数度节点为四个,就是两笔画完。

线性结构

列表

列表和数组

python中的列表和其余语言中的数组很类似,区别为:编程

  • 数组是定长的。
  • 数组的数据类型也必须一致。
  • 对列表或数组来讲,它们的下标操做是最快的。

列表解决的变长问题的方式

  • 假设一开始在内存中分配了四个元素存储的空间,那么前四个元素的append操做不会出现问题。
  • 当第五次append操做时,会先在内存中分配一个可以存储八个元素的空间,也就是翻倍。
  • 而后进行复制,把之前的四个元素依次放到相应的位置上。
  • 若再次超出长度,则继续执行上述操做。
  • 也就是使用了动态表的原理

append操做会不会使速度变慢?数组

  • 根据摊还分析,没有变长时的append和变长时的append均摊,最后的复杂度时O(3).
  • append越日后,变长时的出现频率就会越小
  • 浪费了一部分空间,最坏状况应该是浪费了长度除二减一的空间。

列表解决多数据类型问题的方式

  • 对于纯整数的数组,它的每个元素占4个字节,那么就事先计算好内存分配的大小,计算方法为:- 第一个元素的地址+元素个数 乘 4
  • python的列表里存的不是值,而是指向这个值的内存地址。
  • 地址的大小是同样的,32位里地址是4个字节,64位里地址是8个字节。
  • 这种方法的缺点是内存开销翻倍,这也是python被人诟病的地方。

相关知识点

老是能听到一个词 堆栈 ,堆(heap)和栈(stack)是两个东西,传统的编程语言中把内存分为两个地方,堆空间和栈空间,堆存储的是一些动态生成的对象,与数据结构中的堆是不一样的,栈空间由系统调用,存放函数的参数值,局部变量的值。
应该是早年间翻译的问题,通常听到堆栈指的就是栈。安全

  • 栈是一个数据集合,能够理解为只能在一端进行插入和删除操做的列表。
  • 栈的特色:后进先出(last-in,first-out)
    • 栈顶:操做永远在栈顶。
    • 栈底:最后一个元素。
  • 栈的基本操做:
    • 进栈(压栈):push
    • 出栈:pop
    • 取栈顶: gettop
  • 关于出栈顺序的问题:
    • 对于某个元素,若是进展顺序在它前面的元素出栈时在它后面,那么前面的元素顺序是相反的。
    • 不知道说的明不明白
    • 卡特兰数,n个数的出栈顺序,就是卡特兰数的第n项。
#栈的python实现
class Stack:
    def __init__(self,size):
        self.size=size
        self.top = 0
        self.lst=[]
    
    def push(self,a):
        if self.top = self.size:
            raise StackFullError("stackoverflow") 
        self.lst.insert(self.top,a)
        self.top+=1
    
    def pop(self):
        if self.top = 0:
            raise StackEmptyError()
        b = self.list[self.top]
        self.lst.pop(self.top)
        returm b

栈的应用--括号匹配问题

  • 给定一个字符串,问其中字符串是否匹配。
  • 括号自己知足栈的性质
  • 匹配失败的状况:
    • 括号不匹配
    • 匹配完毕栈没空
    • 栈空了又进元素
    def brace_match(s):
      stack = []
      d ={'(':')','[':']','{':'}'}
      for ch in s:
          if ch in {'(','[','{'}:
              stack.append(ch)
          elif len(stack)==0:
              print('多了%s' %ch)
              return False
          elif d[stack[-1]] == ch:
              stack.pop()
          else:
              print('%s不匹配'%ch)
      if len(stack)==0:
          return True
      else:
          print("未匹配")
          return False

队列

相关知识点:

队列是一个数据集合,仅容许在列表的一端插入,另外一端删除。数据结构

  • 进行插入的时队尾,进行删除操做的是队首,插入和删除操做也被称为进队(push)和出队(pop)。
  • 队列的性质:先进先出(first-in,first-out)
  • 双向队列:两边都能进行插入删除操做的队列。

队列的数组实现:

  • 简单的pop(0)操做复杂度太高,不采用。
  • 因为数组定长,不能继续添加数据,若是是列表,出队的操做就会出现空位,因此想办法让数组变成一个圆环。app

  • 设置两个指针,队首指针front,队尾指针rear。
  • 因为,队列满的时候和队列空的时候rear和front都在一个位置,那么就没法判断了。因而设置成队列满的时候减去一作为队满的标志。
  • 这种队列就叫作环形队列。
    • 当队尾指针front=最大长度+1时,再前进一个位置就自动到0.
    • 实现方式:求余数运算
      • 队首指针前进1:front=(front+1)%maxsize
      • 队尾指针前进1:rear=(rear+1)%maxsize
      • 队空条件:rear=front
      • 队满条件:(rear+1)%maxsize=front
class queue:
    def __init__(self, capacity = 10):
        self.capacity = capacity
        self.size = 0
        self.front = 0
        self.rear = 0
        self.array = [0]*capacity
 
    def is_empty(self):
        return 0 == self.size
 
    def is_full(self):
        return self.size == self.capacity
 
    def enqueue(self, element):
        if self.is_full():
            raise Exception('queue is full')
 
        self.array[self.rear] = element
        self.size += 1
        self.rear = (self.rear + 1) % self.capacity
 
    def dequeue(self):
        if self.is_empty():
            raise Exception('queue is empty')
 
        self.size -= 1
        self.front = (self.front + 1) % self.capacity
 
    def get_front(self):
        return self.array[self.front]

经过两个栈作一个队列的方法

  • 1号栈进栈 模拟进队操做。
  • 2号站出栈,若是2号栈空,把1号站依次出栈并进2号栈,模拟出队操做。
  • 经过摊还分析,时间复杂度仍是O(1)。
class queue:
    def __init__(self,size):
        self.a = []
        self.b = []
        self.size = size

    def popleft(self):
        if not self.b and self.b is None:
            el = self.b.pop(-1)
            self.append(el)
            self.a.pop(-1)
        else:
            raise Exception("empty")

    def append(self,item):
        if self.b<self.size:
            self.b.append[item]
        else:
            raise Exception("FUll")

python关于队列的模块

import queue    #涉及线程安全用queue
from collections import deque   #经常使用解题的用deque

q = deque()     #是一种双向队列,popleft出队

#模拟linux命令 head和tail,假如是tail 5
deque(open('a.text','r',encooding='utf8'),5)
#创建一个定长的队列,当队列满了以后,就会删除第一行,继续添加

链表

相关知识点:

链表就是非顺序表,与队列和栈对应。编程语言

  • 链表中每个元素都是一个对象,每一个对象称为一个节点,包含有数据域key和指向下一个节点的next,经过各个节点之间的相互链接,最终串联成一个链表。

  • 在机械硬盘中,文件就是以链表的形式存储的。
  • 以FAT32为例,文件的单位是文件块(block),一个文件块的大小是4k,一个文件的内容是由链表的方式链接文件块组成的。
  • 链表的第一个节点被称为头节点,数据能够是空的,也能够有值。
  • 头节点为空也是为了表示空链表,也叫作带空节点的链表,头节点也能够记录链表的长度

节点定义

class Node(object):
    def __init__(self,item):
        self.data=data
        self.next=None
#eg
a=Node(1)
b=Node(2)
c=Node(3)
a.next=b
b.next=c    #链表的最后一个节点的next就为None

链表类的实现

class LinkList:
    def __init___(self,li,method='tail'):
        self.head = None
        self.tail = None
        if method == 'head':
            self.create_linklist_head(li)
        if method == 'tail'
            self.create_linklist_tail(li)
        else:
            rais ValueError('unsupport')
            
    #头插法
    def create_linklist_head(self,li):
        self.head = Node(0)
        for v in li:
            n = Node(v)
            n.next = self.head.next     #当插入下一个元素时,应该与下一个节点链接后再跟头节点链接
            self.head.next = n
            self.head.data += 1
    
    #尾插法
    def create_linlist_tail(self,li):        #不断更新尾巴
        self.head = Node(0)
        self.tail = self.head
        for v in li:
            p = Node(v)
            self.tail.next = p
            self.tail = p
            self.head.data += 1
            
    #链表的遍历输出
    def traverse_linlist(self):
        p = self.head.next
        while p:
            yield p.data
            p = p.next

插入删除总结

  • 插入
#p表示待插入节点,curNode表示当前节点
p.next = curNode.next   #不能当前链接直接断开
curNode,next = p
  • 删除
p = curNode.next
curNode.next = p.next
del p   #不写也同样,引用计数,python的内存回收机制

双链表

双链表中每一个节点有两个指针:一个指向后面节点、一个指向前面节点。
节点定义:

class Node(object):
    def __init__(self, item=None):
        self.item = item
        self.next = None
        self.prior = None

双链表的插入和删除

  • 插入
p.next = curNode.next
curNode.next.prior = p
p.prior = curNode
curNode.next = p
  • 删除
p = curNode.next
curNode.next = p.next
p.next.prior = curNode
del p

链表的复杂度分析

链表与列表相比

  • 按元素值查找:列表可使用二分法是O(logn),链表是O(n)
  • 按下标查找:O(1),O(n)
  • 再某元素后插入:O(n),O(1)
  • 删除莫元素:O(n),O(1)
    总的来讲链表再插入和删除某元素的操做时明显快于顺序表,并且经过双链表能够更容易实现栈和队列。

哈希表

直接寻址表

哈希表就是直接寻址表的改进。当关键字的全域U比较小时,直接寻址是一种简单有效的方法。

  • 全域的意思就是它的取值范围。
  • 也就是直接把关键字为key的value放在key的位置上
    直接寻址的缺点:
  • 当域U很大时,须要消耗大量内存。
  • 若是U很大,但关键字不多,浪费大量空间。
  • 若关键字不是数字则没法处理。
    直接寻址表的改进:
  • 构建大小为m的寻址表T
  • key为k的元素放到h(k)上
  • h(k)是一个函数,其将域U映射到表T(0,1,..,m-1)

哈希表

哈希表是一个经过哈希函数计算数据存储位置的线性表的存储结构,又叫作散列表。

  • 哈希表由一个直接寻址表和一个哈希函数组成。
  • 哈希函数h(k)将元素关键字k做为自变量,返回元素的存储下标。
  • 哈希表的基本操做:
    • insert(key,value):插入键值对。
    • get(key):若是存在键为key的键值对则返回其value。
    • delete(key):删除键为key的键值对。

简单哈希函数

  • 除法哈希:h(k)= k mod m
  • 乘法哈希:h(k) = floor(m(KA mod 1)) 0<A<1

哈希表Python实现

class HashTable:
    def __init__(self):
        self.size=11
        self.slots=[None]*self.size
        self.data=[None]*self.size
    def hash_function(self,key,size):
        return key%size
    def rehash(self,old_hash,size):
        return (old_hash+1)%size
    def put(self,key,data):
        hash_value=self.hash_function(key,len(self.slots))
        if self.slots[hash_value]==None:
            self.slots[hash_value]=key
            self.data[hash_value]=data
        else:
            next_slot=self.rehash(hash_value,len(self.slots))
            while self.slots[next_slot]!=None and\
                  self.slots[next_slot]!=key:
                next_slot=self.rehash(next_slot,len(self.slots))
            if self.slots[next_slot]==None:
                self.slots[next_slot]=key
                self.data[next_slot]=data
            else:
                self.data[next_slot]=data
    def get(self,key):
        start_slot=self.hash_function(key,len(self.slots))
        data=None
        stop=False
        found=False
        position=start_slot
        while self.slots[position]!=None and not found and not stop:
            if self.slots[position]==key:
                found=True
                data=self.data[position]
            else:
                position=self.rehash(position,len(self.slots))
                if position==start_slot:
                    stop=True
        return data
    def __getitem__(self,key):
        return self.get(key)
    def __setitem__(self,key,data):
        self.put(key,data)

哈希冲突

因为哈希表的大小是有限的,而要存储信息的数量是无限的,所以,对于任何哈希函数,都会出现两个元素映射到同一个位置的状况,这种状况就叫作哈希冲突。
解决哈希冲突的方法:
开放寻址法:若是哈希函数返回的位置已经有值,则能够向后探查新的位置来储存这个值。

  • 线性探查:若是位置p被占用,则探查 p+1,p+2....
  • 二次探查:若是位置p被占用,则探查p+1**2,p-1**2,p+2**2
  • 二度哈希:有n个哈希函数,当使用第一个哈希函数h1发生冲突时,则使用h2。
  • 哈希表的快速查找能够以空间换时间,须要保证元素个数除以数组容积小于0.5,这个比值就是装载率。
    拉链法:哈希表的每一个位置都链接一个链表,当冲突发生时,冲突的元素被加到该位置链表的最后。
  • 拉链表须要保证每个链表的长度都不要太长。
  • 拉链法的装载率是能够大于一的。
  • 插入、查找等操做的时间复杂度是O(1)的。

哈希在python中的应用

  • 字典和集合都是经过哈希表来实现的
  • 集合能够看做没有value的字典,由于集合也有不重复的性质。
  • 经过哈希函数把字典的键映射为函数:
dic = {'name':'cui'}
#能够认为是h('name')=1,则哈希表为[None,'cui']

树形结构

二叉树

二叉树的节点的节点定义

在堆排序时曾经介绍了什么是二叉树,当时是用列表来实现的,可是二叉树可能出现空值,浪费空间,因此使用相似链表的存储结构。

class BiTreeNode:
    def __init__(self,data):
        self.data=data
        self.lchild=None
        self.rchild=Node

二叉树的遍历

二叉树的遍历有两类四种:

  • 深度优先:前序遍历,中序遍历,后序遍历。
  • 对于有两个遍历求二叉树的方法:前序找根节点(根在前面),中序找左右子树,后序找根节点(根在后面)
#前序遍历,root为根节点
def pre_order(root):
    if root:
    print(root.data,end = '')
    pre_order(root.lchild)
    pre_order(root.rchild)

#中序遍历,若是lchild没值则出栈
def in_order(root):
    if root:
    pre_order(root.lchild)
    print(root.data,end = '')
    pre_order(root.rchild)

#后序遍历,若是rchild没值则出栈
def post_order(root):
    if root:
    pre_order(root.lchild)
    pre_order(root.rchild)
    print(root.data,end = '')
  • 广度优先:层次遍历
#根据队列实现
def level_order(root):
    q=deque()
    q.append(root)
    while(len(q)>0):
        x=q.popleft()
        print(x.data,end='')
        if x.lchild():
            q.append(x.lchild)
        if x.rchild():
            q.append(x.rchild)

二叉搜索树

相关知识点

二叉搜索树,也叫二叉排序树,它要求每个节点左子树的节点都比它小,右子树的节点都比他大。

  • 二叉搜索树的遍历是升序序列
  • 若是y是x左子树的一个节点,那么y.key <=x.key;
  • 若是y是x右子树的一个节点,那么y.key >= x.key;

二叉搜索树的插入

class BST:
    def __init__(self):
        self.root=None  #空不是根节点 而是None
    
    def insert(self,key):
        if not self.root:
            self.root = BiTreeNode(key)
        else:
            p=self.root
            while p:
                if key < p.data:    #分为左子树是否为空的状况
                    if p.lchild:    #左子树有节点就在左子树继续查找,不然就插入左节点的位置
                        p = p.lchild
                    else:
                        p.lchild = BiTreeNode(key)
                elif key > p.data:
                    if p.rchild:    
                        p = p.rchild
                    else:
                        p.lchild = BiTreeNode(key)
                        break
                else:
                    break

二叉搜索树的查找

def query(self,key):
    p = self.root
    while p :
        if key < p.data:
            p = p.lchild
        elif key >p.data:
            p=p.rchild
        else:
            return True
    return False

二叉搜索树的删除

删除有三种状况:

  • 若是要删除的节点是叶子节点,那么找到后直接删除。
  • 若是要删除的节点有一个子节点点,将 此节点的父节点和子节点相链接,而后删除此节点。
  • 若是删除的节点有两个子节点,找到其左子树最大的节点或者右子树的最小节点,删除并替换当前节点,若最后一个一个节点还有一个右子节点,那么再按照第二种状况处理。

二叉搜索树的效率和AVL树

平均状况下,二叉搜索时的时间复杂度为O(logn),可是二叉搜索树可能会出现偏斜的状况,须要采用随机打乱的方法,因此这时候采用AVL树(自动平衡树)。
相关知识点:
AVL树:AVL树是一棵自平衡的二叉搜索树,它具备如下性质:

  • 根的左右子树高度之差的绝对值不能超过1.
    • 计算方法:
    • 每一个节点的左右子树的深度之差,也就是平衡因子。
  • 根的左右子树都是平衡二叉树。

AVL树的插入操做

插入一个节点可能会形成AVL树的不平衡,能够经过旋转操做来修正。
插入一个节点后,只有从插入节点到根节点的路径上的节点的平衡可能被改变,须要找到第一个平衡条件的节点,称之为K,K的两棵子树高度差确定为2.
不平衡的出现有四种状况:

  • 不平衡是因为对K的右子节点的右子树插入致使的:左旋。
  • 不平衡是因为对K的左子节点的左子树插入致使的:右旋。
  • 不平衡是因为右子节点的左子树插入致使的:右旋->左旋。
  • 不平衡是因为左子节点的右子树插入致使的:左旋->右旋。

B树

B-Tree是一种自平衡的多路搜索树,B-Tree存储在硬盘里,用于数据库的索引。

相关文章
相关标签/搜索