数据结构 & 算法(一)

数据结构

什么是数据结构?

 数据结构是计算机存储、组织数据的方式。主要描述数据元素和元素之前的逻辑关系以及在计算机中的存储形式。

数据结构指的是数据元素及数据元素之间的相互关系,或组织数据的形式。
  1. 数据
    数据即信息的载体,是能够输入到计算机中并且能被计算机识别、存储和处理的符号总称。

  2. 数据元素
    数据元素是数据的基本单位,又称之为记录(Record)。一般,数据元素由若干基本项(如姓名、年龄等)组成。

数据之间的结构关系

  1. 逻辑结构
    表示数据之间的抽象关系(如邻接关系、从属关系等),按每个元素可能具有的直接前趋数和直接后继数将逻辑结构分为“线性结构”和“非线性结构”两大类。
    如: 栈、队列等

  2. 物理结构(存储结构)
    逻辑结构在计算机中的具体实现方法,分为顺序存储方法、链接存储方法、散列存储、树状存储等方法。
    如: 线性表、链表、二叉树、B+树、红黑树、图等

逻辑结构和物理结构的关系
每种逻辑结构采用何种特理结构来实现并没有明确的规定,甚至某些特殊情况下,同一种逻辑结构可能需要多种物理结构配合实现。

逻辑结构

  1. 特点:
  • 只是描述数据结构中数据元素之间的联系规律
  • 是从具体问题中抽象出来的数学模型,是独立于计算机存储器的(与机器无关)

逻辑结构分为四种类型:线性结构,树形结构,图形结构,集合结构。

  1. 逻辑结构分类
  • 线性结构

    对于数据结构课程而言,简单地说,线性结构是n个数据元素的有序(次序)集合。

  • 树形结构(层次结构)

    树形结构指的是数据元素之间存在着“一对多”的树形关系的数据结构,是一类重要的非线性数据结构。在树形结构中,树根结点没有前驱结点,其余每个结点有且只有一个前驱结点。叶子结点没有后续结点,其余每个结点的后续节点数可以是一个也可以是多个。

  • 图状结构(网状结构)

    图是一种比较复杂的数据结构。在图结构中任意两个元素之间都可能有关系,也就是说这是一种多对多的关系。

  • 集合结构

    除了以上几种常见的逻辑结构外,数据结构中还包含其他的结构,比如集合等。有时根据实际情况抽象的模型不止是简单的某一种,也可能拥有更多的特征。

物理结构(存储结构)

物理结构又叫存储结构,分为四种:顺序存储结构、链式存储结构、索引结构、散列结构。

  1. 特点:
  • 是数据的逻辑结构在计算机存储器中的映象(或表示)
  • 存储结构是通过计算机程序来实现的,因而是依赖于具体的计算机语言的。
  1. 常用的存储结构
  • 顺序存储
    顺序存储(Sequential Storage):将数据结构中各元素按照其逻辑顺序存放于存储器一片连续的存储空间中。

  • 链式存储
    链式存储(Linked Storage):将数据结构中各元素分布到存储器的不同点,用记录下一个结点位置的方式建立它们之间的联系,由此得到的存储结构为链式存储结构。

线性结构的物理存储

顺序存储结构

数据存储在一段连续的内存空间。

  1. 定义
    若将线性表L=(a0,a1, ……,an-1)中的各元素依次存储于计算机一片连续的存储空间,这种机制表示为线性表的顺序存储结构。

  1. 优点
    1)逻辑上数据元素都是相邻的,请宽度相等,可以用索引随机访问
    2)存储密度高,方便对数据的遍历查找。

  2. 缺点
    对表的插入和删除等运算的效率较差。

程序实现
在Python中,list存放于一片单一连续的内存块,故可借助于列表类型来描述线性表的顺序存储结构,而且列表本身就提供了丰富的接口满足这种数据结构的运算。

>>>L = [1,2,3,4]
>>>L.append(10)      #尾部增加元素
L
[1, 2, 3, 4, 10]

>>>L.insert(1,20)    #插入元素
L
[1, 20, 2, 3, 4, 10]

>>>L.remove(3)       # 删除元素
L
[1, 20, 2, 4, 10]     

>>>L[4] = 30         # 修改
L
[1, 20, 2, 4, 30]

>>>L.index(2)        # 查找
2
>>> del L[1]        # 删除指定位置的数据元素

链式存储结构

链式存储结构是指使用一组不连续的存储单元依次存储各个元素,在每个元素的存储空间内部用额外的空间(指针或引用)来存储各个元素之间的关系。也就是物理上不要求连续,逻辑上连续。

链式存储结构中将每个元素存放在彼此独立的存储单元中,该存储单元称为节点,通常这些节点中增加一个或多个指针来记录前趋或后继节点的地址,以此保证逻辑上的连续。 

数据在不连续的内存空间存储

  1. 特点

    1. 优点
      1)大小动态扩展,存储稀疏,不必开辟整块存储空间。
      2)插入删除效率高

    2. 缺点:
      1)不能随机访问。
      2)查找元素速度慢。

  2. 链表的分类:

    • 单向线性链表
    • 双向线性链表
    • 双向环型链表
  3. 链表的基本操作

    class LinkList:
        def __init__(self, iterable=None):
            self.__head = None
    
        def __str__(self):
            '''转为字符串'''
    
        def append(self, val):
            '''末尾追加'''
    
        def insert(self, index, val):
            '''在index位置插入'''
    
        def __len__(self):
            '''返回数据个数'''
    
        def is_empty(self):
            '判断是否为空'
    
        def clear(self):
            '''清空数据'''
    
        def head_insert(self, val):
            '''头部插入'''
    
        def remove(self, val):
            '''删除指定内容'''
    
        def __getitem__(self, index):
            '''获某个位置的数据'''
    
        def index(self, val):
            '根据val找到索引'
    
  1. 单向线性链表的程序实现

    """
    file: 02_linklist.py
    功能: 实现单链表的构建和功能操作
    重点代码
    """
    
    #  创建节点类
    class Node:
        """
        思路: 将自定义的类视为节点的生成类,实例对象中
            包含数据部分和指向下一个节点的next
        """
    
        def __init__(self, val, next=None):
            self.val = val  # 有用数据
            self.next = next  # 循环下一个节点关系
    
    
    # 做链表操作
    class LinkList:
        """
        思路: 单链表类,生成对象可以进行增删改查操作
            具体操作通过调用具体方法完成
        """
    
        def __init__(self, iterable=None):
            self.__head = None
    
        def __str__(self):
            '''转为字符串'''
            s = "head->"
            p = self.__head
            while p:
                s += "(" + str(p.val) +')->'
                p = p.next
            s += 'None'
            return s
    
        def append(self, val):
            '''末尾追加'''
            if self.__head is None:
                self.__head = Node(val)
                return
            p = self.__head
            while p.next is not None:
                p = p.next
            p.next = Node(val)
    
        def insert(self, index, val):
            '''在index位置插入'''
            if 0 == index:
                return self.__head_insert(val)
            p = self.__head
            for i in range(1, index):  # 让p指向要插入结点的前一个结点
                p = p.next
                if p is None:
                    raise IndexError(str(index) + " is out of range")
    
            temp = Node(val, p.next)
            p.next = temp
    
        def __len__(self):
            '''返回数据个数'''
            p = self.__head
            count = 0
            while p:
                count += 1
                p = p.next
            return count
    
        def is_empty(self):
            '判断是否为空'
            return self.__head is None
    
        def clear(self):
            '''清空数据'''
            self.__head = None
    
        def head_insert(self, val):
            '''头部插入'''
            self.__head = Node(val, self.__head)
    
        def remove(self, val):
            '''删除指定内容'''
            if self.__head.val == val:
                self.__head = self.__head.next
                return
            p = self.__head
            while p.next:
                if p.next.val == val:  # 找到了
                    p.next = p.next.next
                    return
                p = p.next
            raise ValueError(str(val) + ' is not found')
    
    
        def __getitem__(self, index):
            '''获某个位置的数据'''
            p = self.__head
            for _ in range(index):
                if p is None:
                    raise IndexError(str(index) + " is out of range")
                p = p.next
            return p.val
    
        def index(self, val):
            '根据val找到索引'
            p = self.__head
            i = 0
            while p:
                if p.val == val:
                    return i
                p = p.next
                i += 1
            raise ValueError(str(val) + ' is not found')
                
    
    if __name__ == '__main__':
        # da = list()
        da = LinkList()
        da.append(1)
        da.append(3)
        da.append(5)
        da.append(7)
        da.append(9)
        da.remove(1)
        da.remove(5)
        da.remove(9)
        print(da)
        # da.head_insert(0)
        da.insert(1, 2)
        da.insert(3, 4)
        da.insert(4, 10)
        print(da)
    
        print(da[0])
        print(da.index(10))
        print(len(da))
    
  2. 双向线性链表的程序实现

    """
    file: 03_doublelist.py
    功能: 实现单链表的构建和功能操作
    重点代码
    """
    
    
    #  创建节点类
    class Node:
        """
        思路: 将自定义的类视为节点的生成类,实例对象中
            包含数据部分和指向下一个节点的next
        """
    
        def __init__(self, val, next=None, prev=None):
            self.val = val  # 有用数据
            self.next = next  # 循环下一个节点关系
            self.prev = prev
    
    
    # 做链表操作
    class DoubleList:
        """
        思路: 单链表类,生成对象可以进行增删改查操作
            具体操作通过调用具体方法完成
        """
    
        def __init__(self):
            self.__head = None
            self.__tail = None
    
    
        def __str__(self):
            '''转为字符串'''
            s = "head<->"
            p = self.__head
            while p:
                s += "(" + str(p.val) + ')<->'
                p = p.next
            s += 'tail'
            return s
    
        def append(self, val):
            '''末尾追加'''
            if self.__head is None:
                self.__head = self.__tail = Node(val)
                return
            p = self.__head
            while p.next is not None:
                p = p.next
            self.__tail = p.next = Node(val, None, self.__tail.prev)
    
        def head_insert(self, val):
            '''头部插入'''
            if self.__head:  # 有数据
                self.__head = Node(val, self.__head)
            else:
                self.__head = self.__tail = Node(val)
    
    
        def insert(self, index, val):
            '''在index位置插入'''
            if 0 == index:
                return self.head_insert(val)
            elif index < len(self):
                p = self.__head
                for i in range(1, index):  # 让p指向要插入结点的前一个结点
                    p = p.next
                temp = Node(val, p.next, p)
                p.next.prev = temp
                p.next = temp
            else:
                return self.append(val)
    
    
        def __len__(self):
            '''返回数据个数'''
            p = self.__head
            count = 0
            while p:
                count += 1
                p = p.next
            return count
    
        def is_empty(self):
            '判断是否为空'
            return self.__head is None
    
        def clear(self):
            '''清空数据'''
            self.__head = None
            self.__prev = None
    
    
        def remove(self, val):
            '''删除指定内容'''
            if self.__head is None:
                raise ValueError(str(val) + ' is not found')
    
            if self.__head.val == val:
                # next = self.__head.next
                self.__head = self.__head.next
                if self.__head is None:
                    self.__tail = None
                else:
                    self.__head.prev = None
                return
            p = self.__head
            while p.next:
                if p.next.val == val:  # 找到了
                    if p.next is self.__tail:  # 最后一个
                        self.__tail = p
                        p.next = None
                    else:
                        p.next.next.prev = p
                        p.next = p.next.next
                    return
                p = p.next
            raise ValueError(str(val) + ' is not found')
    
        def __getitem__(self, index):
            '''获某个位置的数据'''
            p = self.__head
            for _ in range(index):
                if p is None:
                    raise IndexError(str(index) + " is out of range")
                p = p.next
            return p.val
    
        def index(self, val):
            '根据val找到索引'
            p = self.__head
            i = 0
            while p:
                if p.val == val:
                    return i
                p = p.next
                i += 1
            raise ValueError(str(val) + ' is not found')
    
    
    if __name__ == '__main__':
        # da = list()
        da = DoubleList()
        da.append(1)
        da.append(3)
        da.append(5)
        da.append(7)
        da.append(9)
        da.remove(1)
        da.remove(5)
        da.remove(9)
        print(da)
        # da.head_insert(0)
        da.insert(1, 2)
        da.insert(3, 4)
        da.insert(4, 10)
        print(da)
    
        print(da[0])
        print(da.index(10))
        print(len(da))
    
  3. 双向循环链表的程序实现

    # file: double_circle_link_list.py
    # 自己实现一个双向循环链表,来存储数据
    
    class Node:
        '定义一个节点类,用它来记录数据和数据之间的关系'
        def __init__(self, value):
            self.value = value  # 用于绑定数据的属性
            self.next = self
            self.prev = self
    
    
    class DoubleCircleLinkList:
        '双向循环链表类'
        def __init__(self):
            self.next = self  # 节点的关系都指向自己
            self.prev = self
    
        def __str__(self):
            '''将此对象转化为字符串'''
            s = 'head->'
            p = self.next
            while p is not self:
                s += '(%s)->' % p.value
                p = p.next
            s += 'None'
            return s
    
        def append(self, val):
            '''将数据追加到链表的末尾'''
            tail = self.prev  # 队尾的节点
            p = Node(val)  # 新节点
            p.next = tail.next
            p.prev = tail
            tail.next.prev = p
            tail.next = p
    
        def head_insert(self, val):
            '''向链表的头部插入数据'''
            tail = self  # 队尾的节点
            p = Node(val)  # 新节点
            p.next = tail.next
            p.prev = tail
            tail.next.prev = p
            tail.next = p
    
        def insert(self, index, val):
            '在index位置插入一个节点 val'
            tail = self
            for _ in range(index):
                tail = tail.next
            p = Node(val)  # 新节点
            p.next = tail.next
            p.prev = tail
            tail.next.prev = p
            tail.next = p
    
        def remove(self, val):
            '删出指定的值'
            p = self.next
            # 找要移除的节点
            while p is not self:
                if p.value == val:
                    break
                p = p.next
            if p is self:  # 没找到
                raise ValueError('没找到')
            # 移除p节点
            p.next.prev = p.prev
            p.prev.next = p.next
            p.next = p
            p.prev = p
    
        def __getitem__(self, item):
            pass
    
        def __len__(self):
            count = 0
            p = self.next
            while p is not self:
                count += 1
                p = p.next
            return count
    
    if __name__ == '__main__':
        # da = list()
        da = DoubleCircleLinkList()
        da.append(3)
        da.append(5)
        da.append(9)
        da.insert(2, 7)  # [3, 5, 7, 9]
        da.head_insert(1)  # [1, 3, 5, 7, 9]
        da.remove(5)  # [1, 3, 7, 9]
        print(da)
        # # print(da[0])  # 1
        print('len=', len(da))
    
    

  • 定义
    栈是限制在一端进行插入操作和删除操作的线性表(俗称堆栈),允许进行操作的一端称为“栈顶”,另一固定端称为“栈底”,当栈中没有元素时称为“空栈”。

  • 特点

    1. 栈只能在一端进行数据操作
    2. 栈模型具有先进后出或者叫做后进先出的规律
  • 示意

  • 栈的基本操作:
    栈的操作有入栈(压栈),出栈(弹栈),判断栈的空满等操作。

  • 入栈(push)
    出栈(pop)
    判断栈是否为空(empty)
    查看栈顶元素值(top)

  • 代码实现(顺序存储代码实现)

    '''
    file: 05_list_stack.py
    此实例用顺序存储方式实现栈
    '''
    
    class StackError(Exception):
        pass
    
    class Stack:
        '''用顺序存储实现栈'''
        def __init__(self):
            self.__data = []
    
        def push(self, val):
            '''入栈: 从栈顶插入元素'''
            self.__data.append(val)
    
        def pop(self):
            '''出栈: 从栈顶移除元素,并返回移除的元素'''
            if self.is_empty():
                raise StackError("栈为空,不能移除数据")
            val = self.__data[-1]
            del self.__data[-1]
            return val
    
        def is_empty(self):
            '''判断栈是否为空'''
            return len(self.__data) == 0
    
        def top(self):
            '''查看栈顶元素,但不删除元素'''
            if len(self.__data) == 0:
                raise StackError("栈为空")
            return self.__data[-1]
    
    if __name__ == '__main__':
        Stk = Stack()
        Stk.push(1)
        Stk.push(2)
        Stk.push(3)
        print("栈顶元素是:", Stk.top())  # 3
        while not Stk.is_empty():
            print("弹出:", Stk.pop()) # 3 2 1
    
    
  • 代码实现(链式存储代码实现)

    
    '''
    file: 06_link_stack.py
    此实例用链式存储方式实现栈
    '''
    
    class StackError(Exception):
        pass
    
    class Node:
        def __init__(self, val, next=None):
            self.val = val
            self.next = next
    
    class Stack:
        '''用链式存储实现栈'''
    
        def __init__(self):
            self.__top = None
    
        def push(self, val):  # insert_head(self, val)
            '''入栈: 从栈顶插入元素'''
            self.__top = Node(val, self.__top)
    
        def pop(self):
            '''出栈: 从栈顶移除元素,并返回移除的元素'''
            if self.is_empty():
                raise StackError("栈为空!")
            p = self.__top
            self.__top = self.__top.next
            return p.val
    
        def is_empty(self):
            '''判断栈是否为空'''
            return self.__top is None
    
        def top(self):
            '''查看栈顶元素,但不删除元素'''
            if self.is_empty():
                raise StackError("栈为空")
            return self.__top.val
    
    if __name__ == '__main__':
        Stk = Stack()
        Stk.push(1)
        Stk.push(2)
        Stk.push(3)
        print("栈顶元素是:", Stk.top())  # 3
        while not Stk.is_empty():
            print("弹出:", Stk.pop()) # 3 2 1
    
    

队列

具有先进先出特证的数据结构叫做队列。

队列- FIFO(first in first out) 先进先出

  • 定义
    队列是限制在两端进行插入操作和删除操作的线性表,允许进行存入操作的一端称为"队尾",允许进行删除操作的一端称为"队头"

  • 特点

    1. 队列只能在队头和队尾进行数据操作
    2. 队列模型具有先进先出或者叫做后进后出的规律
  • 示意图

  • 队列的基本操作:
    队列的操作有入队,出队,判断队列的空满等操作

  • 队列的基本操作:
    入队 push
    出队 pop
    判断是否为空 empty
    查看队列首元素 front
    查看队列尾元素 back

  • 应用案例:

  1. 银行取号排序系统。
  2. 打印机的打印任务系统。
  • 队列的代码实现 - 顺序存储

    # file : 07_list_squeue.py
    
    '''队列的实现'''
    
    class Queue:
        '''用顺序存储实现队列'''
        def __init__(self):
            self.__data = []  #
    
        def is_empty(self):
            '''判断队列是否为空'''
            return len(self.__data) == 0
    
        def enqueue(self, val):
            '''入队'''
            self.__data.append(val)
    
        def dequeue(self):
            '''出队'''
            v = self.__data[0]
            del self.__data[0]  # 数据前移,速度慢
            return v
    
    if __name__ == '__main__':
        queue = Queue()
        queue.enqueue(10)
        queue.enqueue(20)
        print("出队元素为: ", queue.dequeue())  # 10
        queue.enqueue(30)
        while not queue.is_empty():
            print("出队:", queue.dequeue())  # 20, 30
    
  • 队列的代码实现 - 链式存储

    # file : 08_list_queue.py
    
    '''队列的实现'''
    
    class Node:  # 结点
        def __init__(self, val, next=None):
            self.val = val
            self.next = next
    class QueueError(Exception):
        pass
    
    class Queue:
        '''用链式存储实现队列'''
        def __init__(self):
            '''约定front 和 rear 要么同时为空,要么同时不为空'''
            self.front = self.rear = None  # 队空
    
        def is_empty(self):
            '''判断队列是否为空'''
            return self.front is None
    
        def enqueue(self, val):
            '''入队'''
            # 1.当队列为空时
            if self.is_empty():
                self.front = self.rear = Node(val)
                return
            # 2. 当队列不为空时
            p = Node(val)
            self.rear.next = p
            self.rear = p
    
        def dequeue(self):
            '''出队'''
            if self.is_empty():
                raise QueueError('队列为空出队失败')
            p = self.front
            self.front = p.next  # self.front.next
            if self.front is None:  # 同时将front 和 rear置空
                self.rear = None
            return p.val
    
    if __name__ == '__main__':
        queue = Queue()
        queue.enqueue(10)
        queue.enqueue(20)
        print("出队元素为: ", queue.dequeue())  # 10
        queue.enqueue(30)
        while not queue.is_empty():
            print("出队:", queue.dequeue())  # 20, 30
    
  • 队列的代码实现 - 双向循环链式存储

    
    # 目标: 使用双向循环链表实现队列
    class Node:
        def __init__(self, val):
            self.value = val
            self.next = self
            self.prev = self
    
    class Queue:
        def __init__(self):
            self.next = self  # self.next 指向对头
            self.prev = self  # self.prev 指向队尾
    
        def is_empty(self):
            return self.next is self
    
        def enqueue(self, val):
            '''入队: 将数据放入队尾'''
            p = Node(val)
            p.next = self
            p.prev = self.prev
            self.prev.next = p
            self.prev = p
    
        def dequeue(self):
            '''出队: 将队头数据弹出'''
            p = self.next
            self.next.next.prev = self
            self.next = self.next.next
            return p.value
    
    
    if __name__ == '__main__':
        q = Queue()
        q.enqueue(10)
        q.enqueue(20)
        q.enqueue(30)
        print('出队:', q.dequeue())  # 10
        q.enqueue(40)
        while not q.is_empty():
            print('出队:', q.dequeue())  # 20,30,40
    

递归

  • 什么是递归?
    所谓递归函数是指一个函数的函数体中直接调用或间接调用了该函数自身的函数。这里的直接调用是指一个函数的函数体中含有调用自身的语句,间接调用是指一个函数在函数体里有调用了其它函数,而其它函数又反过来调用了该函数的情况。

  • 递归定义
    递归用一种通俗的话来说就是自己调用自己,但是需要分解它的参数,让它解决一个更小一点的问题,当问题小到一定规模的时候,需要一个递归出口返回

  • 递归示例

    # 求阶乘
    def fact(n):
        if n == 0:
            return 1
        else:
            return n * fact(n-1)
    
  • 递归原理
    计算机内部使用调用栈来实现递归,后进先出,每当进入递归函数的时候,系统都会为当前函数开辟内存保存当前变量值等信息,每个调用栈之间的数据互不影响,新调用的函数,入栈的时候会放在栈顶

  • 递归函数调用的执行过程分为两个阶段
    1. 递推阶段:从原问题出发,按递归公式递推从未知到已知,最终达到递归终止条件。
    2. 回归阶段:按递归终止条件求出结果,逆向逐步代入递归公式,回归到原问题求解。
  • 优点与缺点
    • 优点:递归可以把问题简单化,让思路更为清晰,代码更简洁
    • 缺点:递归因系统环境影响大,当递归深度太大时,可能会得到不可预知的结果

今日作业

# 1. 用递归的方法求阶乘
# 2. 用链式存储实现栈
# 3. 用链式存储实现队列
# 4. 面试题: 小明爬楼梯,一次只能上1级或者2级台阶,一共有n级台阶,一共有多少种方法上台阶?
"""
思路提示
如果有一级台阶,方法有1种
如果有两级台阶,方法有2种
如果台阶数再增加,大于三个台阶以后,可以认为是只有一二级台阶的一个重复实现
"""
  • 参考答案

    def f(n):
        if n == 1:
            return 1
        if n == 2:
            return 2
        return f(n-1) + f(n-2)
    
    if __name__ == '__main__':
        print(f(5))
    

树形结构

  • 基础概念

  • 定义
    树(Tree)是n(n≥0)个节点的有限集合T,它满足两个条件:有且仅有一个特定的称为根(Root)的节点;其余的节点可以分为m(m≥0)个互不相交的有限集合T1、T2、……、Tm,其中每一个集合又是一棵树,并称为其根的子树(Subtree)。

  • 基本概念
    • 节点


    • 一个节点的子树的个数称为该节点的度数,一棵树的度数是指该树中节点的最大度数。

    • 度数为零的节点称为树叶或终端节点,度数不为零的节点称为分支节点,除根节点外的分支节点称为内部节点。

    • 一个节点的子树之根节点称为该节点的子节点,该节点称为它们的父节点,同一节点的各个子节点之间称为兄弟节点。一棵树的根节点没有父节点,叶节点没有子节点。

    • 一个节点系列k1,k2, ……,ki,ki+1, ……,kj,并满足ki是ki+1的父节点,就称为一条从k1到kj的路径,路径的长度为j-1,即路径中的边数。路径中前面的节点是后面节点的祖先,后面节点是前面节点的子孙。

    • 节点的层数等于父节点的层数加一,根节点的层数定义为一。树中节点层数的最大值称为该树的高度或深度。

    • m(m≥0)棵互不相交的树的集合称为森林。树去掉根节点就成为森林,森林加上一个新的根节点就成为树。

二叉树

  • 定义

    度为2的树称为二叉树。

    二叉树(Binary Tree)是n(n≥0)个节点的有限集合,它或者是空集(n=0),或者是由一个根节点以及两棵互不相交的、分别称为左子树和右子树的二叉树组成。二叉树与普通有序树不同,二叉树严格区分左孩子和右孩子,即使只有一个子节点也要区分左右。

  • 二叉树的特征
    • 二叉树第i(i≥1)层上的节点最多为$2^{i-1} 个。
    • 深度为k(k≥1)的二叉树最多有个节点。
  • 满二叉树 :深度为k(k≥1)时有个节点的二叉树。
  • 完全二叉树 :只有最下面两层有度数小于2的节点,且最下面一层的叶节点集中在最左边的若干位置上。

二叉树的遍历

  • 遍历 :沿某条搜索路径周游二叉树,对树中的每一个节点访问一次且仅访问一次。
  • 基本遍历:
    • 先序遍历: 先访问树根,再访问左子树,最后访问右子树;
    • 中序遍历: 先访问左子树,再访问树根,最后访问右子树;
    • 后序遍历: 先访问左子树,再访问右子树,最后访问树根;
    • 层次遍历: 从根节点开始,逐层从左向右进行遍历。

二叉树的存储结构

1. 二叉树顺序存储

二叉树本身是一种递归结构,可以使用Python list 进行存储。但是如果二叉树的结构比较稀疏的话浪费的空间是比较多的。

  • 空结点用None表示
  • 非空二叉树用包含三个元素的列表[d,l,r]表示,其中d表示根结点,l,r左子树和右子树。
['A',['B',None,None
     ],
     ['C',['D',['F',None,None],
               ['G',None,None],
          ],     
          ['E',['H',None,None],
               ['I',None,None],
          ],
     ]
]
2. 二叉树链式存储
二叉树每个节点中除了存储数据的元素本身外,还需要两个变量来记录当前节点的左子树和右子树。

通常节点定义方式如下

class Node:
    def __init__(self, value, left=None, right=None):
        self.value = value
        self.left = left
        self.right = right

二叉排序树(二叉查找树)- Binary Search Tree

在二叉树中,任何一个子树都满足左子树中所有节点的值都小于它的根节点,所有的右节点的值都大于它的根节点的值的二叉树称为二叉排序树。

  • 作用:主要应用于搜索与查找功能。

示例代码

# 02_binary_sort_tree.py
# 此队列需要自己实现
from double_circle_link_list_queue import Queue

class Node:
    def __init__(self, val, left=None,
                 right=None):
        self.value = val
        self.left = left
        self.right = right


class BinarySortTree:
    def __init__(self):
        self.root = None

    def insert(self, val):
        '将val 值插入树'
        p = Node(val)
        # 当root为空的时候, p就是根节点
        if self.root is None:
            self.root = p
            return
        # 当root 不为空的时候,要将p插入到root节点之后
        self.insert_tree(self.root, p)
        pass

    def insert_tree(self, root_node, new_node):
        '''
        :param root_node: 是树的根节点
        :param new_node:  是新建立的节点
        '''
        if new_node.value < root_node.value:
            # 插入左子树
            if root_node.left is None:  #左为空
                root_node.left = new_node
            else: # 左子树不为空,将new_node 插入到
                  # 以root_node.left 为根的左子树
                self.insert_tree(root_node.left,
                                 new_node)
        else:
            # 插入右子树
            if root_node.right is None:
                root_node.right = new_node
            else:
                self.insert_tree(root_node.right,
                                 new_node)

    def __contains__(self, item):
        ''' in 运算符的重载方法'''
        # print('item = ', item)
        return self.find(self.root, item)

    def find(self, root_node, val):
        'root_node 树的根节点'
        if root_node is None:
            return False
        if root_node.value == val:
            return True
        if val < root_node.value:
            return self.find(root_node.left, val)
        else:
            return self.find(root_node.right, val)

    def preorder_traversal(self):
        self.preorder(self.root)
        print()  # 换行

    def preorder(self, root_node):
        '''先序遍历: 先访问树根,再访问左子树,
                最后访问右子树;'''
        if root_node is None:
            return
        # 如果树不为空,先访问根
        print(root_node.value, end=' ')
        # 再访问左子树
        self.preorder(root_node.left)
        # 最后访问右子树;
        self.preorder(root_node.right)

    def middleorder_traversal(self):
        self.middleorder(self.root)
        print()  # 换行

    def middleorder(self, root_node):
        '''中序遍历: 先访问左子树,再访问树根,
        最后访问右子树;'''
        if root_node is None:
            return
        # 如果树不为空,先访问左子树
        self.middleorder(root_node.left)
        # 再访问树根
        print(root_node.value, end=' ')
        # 最后访问右子树;
        self.middleorder(root_node.right)

    # - 后序遍历: 先访问左子树,再访问右子树,最后访问树根;
    def Level_traversa(self):
        '''层次遍历:  从根节点开始,逐层从左向右
        进行遍历。
        使用队列来实现
        '''
        if self.root is None: # 根为空,不需要遍历
            return;
        queue = Queue()
        queue.enqueue(self.root)  # 把根节点入队
        while not queue.is_empty():
            # 出队一个节点
            p = queue.dequeue()
            print(p.value, end=' ')
            if p.left:
                queue.enqueue(p.left)
            if p.right:
                queue.enqueue(p.right)
        print()  # 换行



if __name__ == '__main__':
    tree = BinarySortTree()
    tree.insert(5)
    tree.insert(8)
    tree.insert(3)
    tree.insert(4)
    tree.insert(9)
    tree.insert(7)
    print('2 in tree', 2 in tree)  # False
    print('7 in tree', 7 in tree)  # True
    print('先序遍历')
    tree.preorder_traversal()
    print('中序遍历')
    tree.middleorder_traversal()

    print('层次遍历')
    tree.Level_traversa()
    # 5 3 8 4 7 9
    print("OK")

练习:

  1. 在某饭店,每次吃饭的小票积赞到1000元就可以换一个免费的菜。
    写一个程序,把每次的小票的钱数输入电脑,找到这些小票相加之和大于1000元且又不浪费的最好组合。打印出这些组合。

  2. 定义一个栈,输入一些数压入栈中,每次打印栈内元素。
    当栈满后,结束输入,将栈内的数据依次弹出并打印,然后程序退出。

算法

  • 全场动作必须跟我整齐划一,来,我们一起来做一道题
    若n1 + n2 + n3=1000, 且n1^2 + n2^2 = n3^2 (n1,n2,n3为自然数),求出所有n1、n2、n3可能的组合
    
    # 思路1:
    n1 = 0
    n2 = 0 
    n3 = 0
    # 判断n1+n2+n3是否等于1000,之后变n3=1,n3=2,n3=3,... 然后再变n2
    
  • 代码实现
    import time
    
    start_time = time.time()
    for n1 in range(0,1001):
         for n2 in range(0,1001):
              for n3 in range(0,1001):
                   if n1 + n2 + n3 == 1000 and n1**2 + n2**2 == n3**2:
                        print('[%d,%d,%d]' % (n1,n2,n3))
    end_time = time.time()
    print('执行时间:%.2f' % (end_time-start_time))  # 此处大约运行180秒
    
    • 思考:
      上述程序 如果变为 n1+n2+n3=2000 了呢?
  • 算法概念
    是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制。也就是说,能够对一定规范的输入,在有限时间内获得所要求的输出
  • 算法五大特性
    1. 输入 -- 具有0个或多个输入
    2. 输出 -- 至少由1个或者多个输出
    3. 有穷性 -- 算法执行的步骤是有限的
    4. 确定性 -- 每个计算步骤无二义性
    5. 可行性 -- 每个计算步骤能够在有限的时间内完成

时间复杂度概述

  • 时间复杂度 - 前序

    # 各位,一万年太久,只争朝夕,来提升一下上题的效率吧!!!
    start_time = time.time()
    
    for n1 in range(0,1001):
         for n2 in range(0,1001):
              n3 = 1000 - n1 - n2
              if n3 >= 0 and n1**2 + n2**2 == n3**2:
                   print('[%d,%d,%d]'%(n1,n2,n3))
    end_time = time.time()
    print('执行时间:%.2f' % (end_time-start_time))  # 此处大约运行0.72秒
    
  • 总结与思考
    解决同一个问题有多种算法,但是效率有区别,那么如何衡量呢?

    1. 执行时间反应算法效率 - 绝对靠谱吗?
      不是绝对靠谱: 因机器配置有高有低,不能冒然绝对去做衡量

    2. 那如何衡量更靠谱???
      运算数量 - 执行步骤的数量

时间复杂度概念

同一个算法,由于机器配置差异,每台机器执行的总时间不同,但是执行基本运算的数量大体相同,所以把算法执行步骤的数量称为时间复杂度

  • 总结
    时间复杂度:程序执行步骤的数量

  • 时间复杂度 - 大O表示法前序

    # ###############################################################
    for n1 in range(0,1001):
         for n2 in range(0,1001):
         for n3 in range(0,1001):
              if n1 + n2 + n3 == 1000 and n1**2 + n2**2 == n3**2:
                   print('[%d,%d,%d]' % (n1,n2,n3))
    # ###############################################################
                   
    # 计算时间复杂度 - 执行计算步骤的次数
    T = 1000 * 1000 * 1000 * 2
    T = n * n * n * 2
    T(n) = n ** 3 * 2  --> 则时间复杂度为T(n) 及 n**3 * 2
    
  • 总结: 什么是时间复杂度?
    衡量标准: 运算步骤来衡量,n代表解决问题的规模问题,对于同一类问题所花费的步骤有一个统一的表示,这个T(n)为时间复杂度,n**3为它的大O表示法,即O(n^3)

  • 时间复杂度表示 - 大O表示法
    1.需要理解
    假定计算机执行算法每个基本操作的时间是固定的一个时间单位,则有多少个基本操作就代表会花费多少时间单位。虽然对于不同机器环境切确的时间单位不同,但对于算法进行的基本操作数量在规模数量级上相同,因此可以忽略机器环境影响而客观反映算法的时间效率,用"大O记法"表示

    1. 需要记忆
      时间复杂度:假设存在函数g,使得算法A处理规模为n的问题所用时间为T(n)=O(g(n)),则称O(g(n))为算法A的渐近时间复杂度,简称时间复杂度,记为T(n)
    2. 大O表示法
      对算法进行特别具体细致分析虽然好,但实践中实际价值有限。对我们来说算法的时间性质和空间性质最重要的是数量级和趋势,这些是分析算法效率的主要部分。
      所以忽略系数,忽略常数,比如5n^2 和 100n2属于一个量级,时间复杂度为O(n2)
  • 时间复杂度分类

    1.分类
    1)最优时间复杂度 - 最少需要多少个步骤
    2)最坏时间复杂度 - 最多需要多少个步骤
    3)平均时间复杂度 - 平均需要多少个步骤
    我们平时所说的时间复杂度,指的是最坏时间复杂度
    
    2.示例 - 列表元素排序
    [3,1,4,1,5,9,2,6]   --> 时间复杂度:O(n^2)
    [1,2,3,4,5,6,7,8]   --> 时间复杂度:O(n)
          for i in L:
              先扫描一遍,若有序直接退出
              时间复杂度变为 n
    
  • 时间复杂度 - 计算规则

    1. 基本操作,只有常系数,认为其时间复杂度为O(1)
    顺序/条件/循环 - 所有语言都包括
    顺序 - 基本步骤之间的累加
    print('abc') -> O(1)
    print('abc') -> O(1)
    2. 循环: 时间复杂度按乘法进行计算
    3. 分支: 时间复杂度取最大值(哪个分支执行次数多算哪个)
    
    练习:请计算如下代码的时间复杂度
    for n1 in range(0,1001):
         for n2 in range(0,1001):
         n3 = 1000 - n1 - n2
         if n1**2 + n2**2 == n3**2:
              print('[%d,%d,%d]'%(n1,n2,n3))
              
    T(n) = n * n * (1+max(1,0))
    T(n) = n**2 * 2
    T(n) = n**2
    T(n) = O(n**2)
    用大O表示法表示为 O(n^2)
    
  • 常见时间复杂度

    执行次数 时间复杂度
    20(20个基本步骤) O(1) 常数阶
    8n+6 O(n) 线性阶
    2n^2 + 4n + 2 O(n^2) 平方阶
    8logn + 16 O(logn) 对数阶
    4n + 3nlogn + 22 O(nlog(n)) nlog阶
    2n^3 + 2n^2 + 4 O(n^3) 立方阶
    2 ^ n O(2^n) 指数阶
    # 所消耗的时间从小到大
    O(1) O(1)
    O(2n+1)       --> O(n)
    O(n**2+n+1)   --> O(n**2)
    O(3n**3+1)    --> O(n**3)
    
  • 时间复杂度计算

算法效率——用依据该算法编制的程序在计算机上执行所消耗的时间来度量。“O”表示一个数量级的概念。根据算法中语句执行的最大次数(频度)来 估算一个算法执行时间的数量级。

计算方法:

  • 写出程序中所有运算语句执行的次数,进行加和
  • 如果得到的结果是常量则时间复杂度为1
  • 如果得到的结果中存在变量n则取n的最高次幂作为时间复杂度

下图表示随问题规模n的增大,算法执行时间的增长率。

算法基础概念

  1. 定义
    算法(Algorithm)是一个有穷规则(或语句、指令)的有序集合。它确定了解决某一问题的一个运算序列。对于问题的初始输入,通过算法有限步的运行,产生一个或多个输出。

    数据的逻辑结构与存储结构密切相关:

    • 算法设计: 取决于选定的逻辑结构
    • 算法实现: 依赖于采用的存储结构
  1. 算法的特性
  • 有穷性 —— 算法执行的步骤(或规则)是有限的;
  • 确定性 —— 每个计算步骤无二义性;
  • 可行性 —— 每个计算步骤能够在有限的时间内完成;
  • 输入 ,输出 —— 存在数据的输入和出输出
  1. 评价算法好坏的方法
  • 正确性:运行正确是一个算法的前提。
  • 可读性:容易理解、容易编程和调试、容易维护。
  • 健壮性:考虑情况全面,不容以出现运行错误。f
  • 时间效率高:算法消耗的时间少。
  • 储存量低:占用较少的存储空间。

参考网站: https://www.cs.usfca.edu/~galles/visualization/Algorithms.html

排序

排序(Sort)是将无序的记录序列(或称文件)调整成有序的序列。

常见排序方法:

1. 冒泡排序(Bubble Sort)

冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。

2. 选择排序(Selection Sort)

选择排序(Selection sort)是一种简单直观的排序算法。其基本思想是:首先在未排序的数列中找到最小(or最大)元素,然后将其存放到数列的起始位置;接着,再从剩余未排序的元素中继续寻找最小(or最大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

3. 插入排序

插入排序有两种:
     1. 直接插入排序(Insertion Sort)
     2. 希尔排序(Shell Sort)

直接插入排序是将无序序列中的数据插入到有序的序列中,在遍历无序序列时,首先拿无序序列中的首元素去与有序序列中的每一个元素比较并插入到合适的位置,一直到无序序列中的所有元素插完为止。

4. 快速排序

实现步骤:

从数列中挑出一个元素,称为 "基准"(pivot),
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

示例代码

#! /usr/bin/env python3


def bubble_sort(lst):
    '''冒泡排序:
    每次都把最大的数放到最右侧
    时间复杂度: O(n^2)
    '''
    for j in range(len(lst), 1, -1):  # j代表要排序数据的个数
        for i in range(j-1):
            if lst[i] > lst[i+1]:
                lst[i], lst[i+1] = lst[i+1],lst[i]

def selection_sort(lst):
    '''选择排序'''
    if not lst:   # 如果L 为空,则直接返回
        return

    for i in range(len(lst)):
        m = i  # 先假设i位置的值最小, m记录最小的位置
        # 查找之后的哪个值比i位置的小,用j记录下来
        for j in range(i, len(lst)):
            if lst[j] < lst[m]:
                m = j  # i 标记更小的位置
        # 当循环过后,m 指定的是最小的数据
        if i != m:  # 如果m变动则交换数据
            lst[m], lst[i] = lst[i], lst[m]

def insertion_sort(lst):
    '''直接插入排序'''
    for i in range(1, len(lst)):
        # i 未排序数据的最前面一个数的索引
        # 逆向遍历比较找到其位置实现插入
        j = i - 1  # j代表已经排好序的数的最后一个数的索引
        temp = lst[i]  # 取出的元素
        while lst[j] > temp and j >= 0:
            # 当j对应的值大于,未排序的第一个数时
            lst[j+1] = lst[j]
            j -= 1  #
        # 此时j+1的位置是空位,将temp放入空位
        lst[j+1] = temp

def quick_sort(lst, low_index=0, high_index=None):
    '''快速排序
    lst: 要排序的列表
    low_index: 要排序的起点位置
    high_index: 要排序的终点位置,如果没有给出,则默认为列表末尾
    '''
    if high_index is None:
        high_index = len(lst) - 1

    if low_index >= high_index:  # 没有数据需要排序
        return

    low = low_index  # 起点设置为当前起点
    high = high_index  # 终点设置为当前终点
    x = lst[low]  # 先假设第一个数据是要参照的数据

    # 当low 小于 hight, 开始循环
    while low < high:
        # 1. 让hight 向左走
        # 1.1 当low >= hight  停止
        # 1.2  当L[hight] 的值 小于x 进行操作后停止
        while low < high:
            if lst[high] < x:
                lst[low] = lst[high]
                low += 1
                break
            high -=1

        # 2. 让 low 向右走
        # 2.1 当 hight <= low, 停止
        # 2.2 当L[low] 的值 大于x 进行操作后停止
        while low < high:
            if lst[low] > x:
                lst[high] = lst[low]
                high -=1
                break
            low += 1

    lst[low] = x  # 此时 low 一定等于 high

    quick_sort(lst, low_index, low-1)
    quick_sort(lst, high+1, high_index)


def main():
    L = [5, 8, 2, 4, 9, 1, 7]
    print(L)
    bubble_sort(L)
    # quick_sort(L)
    # insertion_sort(L)
    # selection_sort(L)
    print(L)


if __name__ == "__main__":
    main()

查找

查找(或检索)是在给定信息集上寻找特定信息元素的过程。

二分法查找

当数据量很大适宜采用该方法。采用二分法查找时,数据需是排好序的。

  • 二分查找图解一

  • 二分查找图解二

# file : 02_binary_find.py
# 二分查找算法实现。
# 如果成功返回数组元素的下标,失败返回-1

def binary_find(lst, key):
    low = 0
    high = len(lst)-1
    while low <= high:  # 这里要把等号等号包含进来
        mid = (low + high) // 2
        if key == lst[mid]:
            return mid
        elif key > lst[mid]:
            low = mid + 1
        elif key < lst[mid]:
            high = mid - 1

    raise IndexError("没有制定的值!")


def main():
    L = [1, 3, 5, 7, 9, 11, 13, 17, 19]  # list(range(1, 20, 2))
    print('19的索引是:', binary_find(L, 19))


if __name__ == "__main__":
    main()

你可能感兴趣的:(数据结构 & 算法(一))