Python基础学习笔记 —— 数据结构与算法_数据结构与算法python笔记

28 阅读15分钟

img img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

单链表的每个结点的指针域只指向下一个结点,整个链表是无环的

在这里插入图片描述

双向链表

在这里插入图片描述

单向循环链表

在这里插入图片描述

相比于数组,在链表中执行插入、删除等操作可以使得操作效率大大提高。

以单链表为例,在数组内如想要删除或者插入元素到某一位置,该位置之后的所有元素都需要象前或者向后移动,这样一来,时间复杂度就与数组的长度有关,为O(n);但是在单链表中,仅仅需要通过改变所要删除或者插入位置前后结点的指针域即可,时间复杂度为O(1)。

2. 单链表的实现与基本操作

# 链表结点
class Node(object):
    def \_\_init\_\_(self, item):
        self.item = item
        self.next = None


# 单链表
class SingleLink(object):
    # 选择该初始化方法,调用时使用 SingleLink(node)
    def \_\_init\_\_(self, node=None):
        self.head = node

    # 选择该初始化方法,调用时就不能使用上面的 SingleLink(node),初始化只能通过append添加结点
    # def \_\_init\_\_(self):
    # self.head = None

    # 判断单链表是否为空
    def is\_empty(self):
        if self.head is None:
            return True
        else:
            return False

    # 获取链表长度
    def length(self):
        cur = self.head
        count = 0
        while cur is not None:
            cur = cur.next
            count += 1
        return count

    # 遍历链表
    def travel(self):
        cur = self.head
        while cur is not None:
            print(cur.item, end=" ")
            cur = cur.next

    # 链表头部增加结点
    def add(self, item):
        node = Node(item)
        node.next = self.head
        self.head = node

    # 链表尾部增加结点
    """
 注意:如果链表为空链表,cur是没有next的,只需self.head=node
 """

    def append(self, item):
        node = Node(item)
        if self.is_empty():
            self.head = node
        else:
            cur = self.head
            while cur.next is not None:
                cur = cur.next
            cur.next = node

    # 链表指定位置增加结点
    """
 注意:这个只适用于链表中不存在重复元素的,要区别LeetCode203. 移除链表元素(这个题链表里会存在重复元素)
 """
    def insert(self, pos, item):
        if pos == 0:
            self.add(item)
        elif pos >= self.length():
            self.append(item)
        else:
            node = Node(item)
            cur = self.head
            count = 0
            while count < pos - 1:
                cur = cur.next
                count += 1
            node.next = cur.next
            cur.next = node

    # 删除结点
    def remove(self, item):
        cur = self.head
        pre = None
        while cur is not None:
            # 找到了要删除的元素
            if cur.item == item:
                # 如果要删除的位置在头部
                if cur == self.head:
                    self.head = cur.next
                # 要删除的位置不在头部
                else:
                    pre.next = cur.next
                return  # 删除元素后及时退出循环
            # 没有找到要删除的元素
            else:
                pre = cur
                cur = cur.next

    # 查找结点
    def search(self, item):
        cur = self.head
        while cur is not None:
            if cur.item == item:
                return True
            cur = cur.next
        return False

1.3 队列

1. 队列的基本结构

队列最基本的特点就是先进先出,在队列尾部加入新元素,在队列头部删除元素,分为双端队列和一般的单端队列。

队列的作用:对于任务处理类的系统,即先把用户发起的任务请求接收过来存到队列中,然后后端开启多个应用程序从队列中取任务进行处理,队列起到了 缓冲压力 的作用

2. 队列的实现与基本操作

利用列表来简单地模拟队列

class Queue(object):
    def \_\_init\_\_(self):
        self.items = []

    # 入队
    def enqueue(self, item):
        self.items.append(item)

    # 出队
    def dequeue(self):
        self.items.pop(0)

    # 队列的大小
    def size(self):
        return len(self.items)

    # 判断队列是否为空
    def is\_empty(self):
        return self.items == []

对于队列这种数据结构,Python的 queue 类模块中提供了一种先进先出的队列模型 Queue,可以限制队列的长度也可以不限制,在创建队列时利用 Queue(maxsize=0),maxsize小于等于0表示不限制,否则表示限制。

我们在编程的过程中也可以通过调用现有类来实现队列

from queue import Queue

# 队列的定义
q = Queue(maxsize=0)

# put() 在队列尾部添加元素
q.put(1)
q.put(2)
# print(q) # <queue.Queue object at 0x0000020095EE82B0>
# print(q.queue) # deque([1, 2])

# get() 在队列头部取出元素,返回队列头部元素
q.get()
print(q.queue)  # deque([2])

# empty() 判断队列是否为空
print(q.empty())  # False

# full(0 判断队列是否达到最大长度限制
print(q.full())  # False

# qsize() 队列当前的长度
print(q.qsize())  # 1

3. 双端队列的实现与基本操作

双端队列(deque,全名double-ended queue ), 是一种具有队列和栈的性质的数据结构
双端队列中的元素可以从两端弹出,其限定插入和删除操作在表的两端进行。双端队列可以在队列任意一端入队和出队。

class deque(object):
    def \_\_init\_\_(self):
        self.items = []

    # 判断是否为空
    def is\_empty(self):
        return self.items == []

    # 队列的大小
    def size(self):
        return len(self.items)

    # 头部添加数据
    def add\_front(self, item):
        self.items.insert(0, item)

    # 尾部添加数据
    def add\_rear(self, item):
        self.items.append(item)

    # 头部删除数据
    def remove\_front(self):
        self.items.pop(0)

    # 尾部删除数据
    def remove(self):
        self.items.pop()

1.4 栈

1. 栈的基本结构

栈最突出的特点是先进后出,其插入、删除操作均在栈顶进行。栈一般包括入栈、出栈操作,并且有一个顶指针(top)用于指示栈顶的位置

2. 栈的实现与基本操作

class Stack(object):
    def \_\_init\_\_(self):
        self.items = []  

    # 进栈
    def push(self, item):
        self.items.append(item)

    # 出栈
    def pop(self):
        self.items.pop()

    # 遍历
    def travel(self):
        for i in self.items:
            print(i)

    # 栈的大小
    def size(self):
        return len(self.items)

    # 栈是否为空
    def is\_empty(self):
        return self.items == []
        # return len(self.items) == 0

    # 返回栈顶元素
    def peek(self):
    	if self.is_empty():
    		return "栈空"
        return self.items[self.size()-1]
        # return self.items[-1]

1.5 堆

Python 中 heapq 模块是 小顶堆

实现 大顶堆 方法: 小顶堆的插入和弹出操作均将元素 取反 即可

from heapq import \*
from random import shuffle

data = list(range(10))
shuffle(data)
print(f'原始数据为:{data}')
small_heap = []
for num in data:
    heappush(small_heap, num)
print(f'创建的小顶堆为:{small\_heap}')

heap = []
for num in data:
    heappush(heap, -num)
print(heap)
big_heap = [-heappop(heap) for _ in range(len(heap))]
print(f'输出的大顶堆为:{big\_heap}')

# 总体思路:负负得正

参考

1.6 二叉树

1. 树

树是一种数据结构,它是由 n 个有限结点组成的一个具有层次关系的集合。

树的基本性质如下:

在这里插入图片描述

2. 二叉树的基本结构

二叉树则是每个结点最多有两个子树的树结构,通常子树被称作“左子树”和“右子树”。

二叉树的一般性质:

  • 二叉树是有序的(左右子树不能颠倒)

  • 二叉树的第 k 层上的结点数目最多为

2

k

1

2^{k-1}

2k−1

  • 深度为 h 的二叉树最多有

2

h

1

2^h-1

2h−1 个结点

  • 设非空二叉树中度为0、1 和 2 的结点个数分别为

n

0

n_0

n0​ 、

n

1

n_1

n1​ 和

n

2

n_2

n2​,则

n

0

=

n

2

1

n_0 = n_2+1

n0​=n2​+1(叶子结点比二分支结点多一个)

注意:这里的层数 k ,深度 h 均是从 1 开始的

其他常见的二叉树:

在这里插入图片描述

注意:

  • 如果按层序从0开始编号,结点 i 的左孩子为:2i+1,结点 i 的右孩子为:2i+2,结点 i 的父结点为:(i-1)//2

  • 如果结点按层序从0开始编号,假设共有 n 个结点,若 i <= n//2-1 ,该结点为非终端结点,若 i > n//2-1 ,该结点为终端结点

在这里插入图片描述

二叉树通常以链式存储

3. 二叉树的实现与基本操作

# 定义结点类
class Node(object):
    def \_\_init\_\_(self, item):
        self.item = item
        self.lchild = None
        self.rchild = None


# 定义二叉树
class BinaryTree(object):
    def \_\_init\_\_(self, node=None):
        self.root = node

    """
 思路分析:首先在队列中插入根结点,取出该结点,再判断该结点的左右子树是否为空,
 左子结点不空,将其入队,右子结点不空,将其入队,
 再分别判断左右结点的左右子结点是否为空,
 循环往复,直到发现某个子结点为空,即把新结点添加进来
 """

    # 添加结点
    def add(self, item):
        node = Node(item)
        # 二叉树为空
        if self.root is None:
            self.root = node
            return

        # 二叉树不空
        queue = []
        queue.append(self.root)
        # 编译环境会提示,也可以直接写成:queue = [self.root]

        while True:
            # 从队头取出数据
            node1 = queue.pop(0)
            # 判断左结点是否为空
            if node1.lchild is None:
                node1.lchild = node
                return
            else:
                queue.append(node1.lchild)
            # 判断右结点是否为空
            if node1.rchild is None:
                node1.rchild = node
                return
            else:
                queue.append(node1.rchild)

    # 广度优先遍历,也叫层次遍历
    def breadth(self):
        if self.root is None:
            return

        queue = []
        queue.append(self.root)
        while len(queue) > 0:
            # 取出数据
            node = queue.pop(0)
            print(node.item, end=" ")

            # 判断左右子结点是否为空
            if node.lchild is not None:
                queue.append(node.lchild)
            if node.rchild is not None:
                queue.append(node.rchild)

    # 深度优先遍历
    # 先序遍历(根左右)
    def preorder\_travel(self, root):
        if root is not None:
            print(root.item, end=" ")
            self.preorder_travel(root.lchild)
            self.preorder_travel(root.rchild)

    # 中序遍历(左根右)
    def inorder\_travel(self, root):
        if root is not None:
            self.inorder_travel(root.lchild)
            print(root.item, end=" ")
            self.inorder_travel(root.rchild)

    # 后序遍历(左右根)
    def postorder\_travel(self, root):
        if root is not None:
            self.postorder_travel(root.lchild)
            self.postorder_travel(root.rchild)
            print(root.item, end=" ")


if __name__ == "\_\_main\_\_":
    tree = BinaryTree()
    tree.add(1)
    tree.add(2)
    tree.add(3)
    tree.add(4)

    # 添加结点的代码逻辑就是将添加第一个结点设置为根结点
    print(tree.root)
    print()

    # 层序遍历
    tree.breadth()  # 1 2 3 4
    print()
    # 前序遍历(根左右)
    tree.preorder_travel(tree.root)  # 1 2 4 3
    print()

    # # 中序遍历(左根右)
    tree.inorder_travel(tree.root)  # 4 2 1 3
    print()

    # # 后序遍历(左右根)
    tree.postorder_travel(tree.root)  # 4 2 3 1


注意:

  • 广度优先遍历基于队列

  • 深度优先遍历基于栈

试试 LeetCode 相关题目吧

注意:二叉树遍历相关的题目在LeetCode环境中,省略了添加结点的代码逻辑,以及定义二叉树类中的初始化,要注意区分、根据具体题目应变。另外,其输入输出都是列表的形式,要注意

4. 由遍历结果反推二叉树结构

在这里插入图片描述

2 排序算法

算法的稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变则称这种排序算法是稳定的,否则称为不稳定的

  • 不稳定的排序算法:选择排序、快速排序、希尔排序、堆排序
  • 稳定的排序算法:冒泡排序、插入排序、归并排序、基数排序

2.1 冒泡排序

对要进行排序的数据中相邻的数据进行两两比较,将较大的数据放在后面,依次对所有的数据进行操作,直至所有数据按要求完成排序

如果有n个数据进行排序,总共需要比较 n-1 次

每一次比较完毕,下一次的比较就会少一个数据参与

在这里插入图片描述

def bubble\_sort(lis):
    n = len(lis)
    # 控制比较的轮数
    for j in range(n - 1):
        count = 0
        # 控制每一轮的比较次数
        # -1是为了让数组不要越界
        # -j是每一轮结束之后, 我们就会少比一个数字
        for i in range(n - 1 - j):
            if lis[i] > lis[i + 1]:
                lis[i], lis[i + 1] = lis[i + 1], lis[i]
                count += 1
        # 算法优化
        # 如果遍历一遍发现没有数字交换,退出循环,说明数列是有序的
        if count == 0:
            break


if __name__ == "\_\_main\_\_":
    lis = [2, 7, 3, 6, 9, 4]
    bubble_sort(lis)
    print(lis)

总结:

  • 冒泡排序是稳定的

  • 最坏时间复杂度为

O

(

n

2

)

O(n^2)

O(n2)

  • 最优时间复杂度为

O

(

n

)

O(n)

O(n),遍历一遍发现没有任何元素发生了位置交换终止排序

2.2 快速排序

快速排序算法中,每一次递归时以第一个数为基准数 ,找到数组中所有比基准数小的。再找到所有比基准数大的。小的全部放左边,大的全部放右边,确定基准数的正确位置。

def quick\_sort(lis, left, right):
    # 递归的结束条件:left > right
    if left > right:
        return

    # 存储临时变量,left0始终为0,right0始终为len(lis)-1
    left0 = left
    right0 = right

    # 基准值
    base = lis[left0]

    # left != right
    while left != right:
        # 从右边开始找寻小于base的值
        while lis[right] >= base and left < right:
            right -= 1

        # 从左边开始找寻大于base的值
        while lis[left] <= base and left < right:
            left += 1

        # 交换两个数的值
        lis[left], lis[right] = lis[right], lis[left]

    # left=right
    # 基准数归位
    lis[left0], lis[left] = lis[left], lis[left0]

    # 递归操作
    quick_sort(lis, left0, left - 1)
    quick_sort(lis, left + 1, right0)  # quick\_sort(lis, left + 1, right0)


if __name__ == '\_\_main\_\_':
    lis = [1, 2, 100, 50, 1000, 0, 10, 1]
    quick_sort(lis, 0, len(lis) - 1)
    print(lis)

总结:

  • 快速排序算法不稳定

  • 最好的时间复杂度:

O

(

n

l

o

g

2

n

)

O(nlog_2n)

O(nlog2​n),初始序列大小均匀,每一次选择的基准值将待排序的序列划分为均匀的两部分,递归深度最小,算法效率最高

  • 最坏的时间复杂度:

O

(

n

2

)

O(n^2)

O(n2),初始序列有序或逆序,每次选择的基准值都是靠边的元素,递归深度最大,算法效率最低

2.3 (简单)选择排序

第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾,以此类推,直到全部待排序的数据元素的个数为零。

在这里插入图片描述

def select\_sort(lis):
    n = len(lis)
    # 控制比较的轮数
    for j in range(n - 1):
        # 假定最小值的下标
        min_index = j
        # 控制每一轮的比较次数
        for i in range(j + 1, n):
        	# 进行比较获得最小值下标
            if lis[min_index] > lis[i]:
            	min_index = i
        # 如果假定的最小值下标发生了变化,那么就进行交换
        if min_index != j:
            lis[min_index], lis[j] = lis[j], lis[min_index]


if __name__ == "\_\_main\_\_":
    lis = [2, 7, 3, 6, 9, 4]
    select_sort(lis)
    print(lis)

总结:

  • 选择排序是不稳定的

  • 最坏时间复杂度为O(n^2)

  • 最优时间复杂度为O(n^2)

2.4 堆排序

堆排序是指利用堆(必须是一种完全二叉树)这种数据结构所设计的一种排序算法

其核心思想是:

  • 建立(大或小)根堆:
    • 从最后一个非终端结点开始,把所有的非终端结点都检查一遍,是否满足(大或小)根堆的要求,如不满足,则与更(大或小)的结点进行交换,元素互换可能会破坏下一层的堆,需要采用相同的方法进行调整,得到(大或小)根堆。
  • 排序:
    • 每一趟排序将堆顶元素加入有序子序列(堆顶元素与待排序序列中最后一个元素交换),并将待排序元素序列再次调整为(大或小)根堆

堆排序有以下两种:

  • 大根堆:每个结点的值都大于等于其左右孩子结点的值,满足arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2],基于大根堆的堆排序得到递增序列
  • 小根堆:每个结点的值都小于等于其左右孩子结点的值,满足arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2],基于小根堆的堆排序得到递减序列

注意:

  • 如果结点按层序从0开始编号,结点 i 的左孩子为:2i+1,结点 i 的右孩子为:2i+2,结点 i 的父结点为:(i-1)/2

  • 如果结点按层序从0开始编号,假设共有 n 个结点,若 i <= n/2-1 ,该结点为非终端结点,若 i > n/2-1 ,该结点为终端结点

以大根堆为例,小根堆同理

def adjust(arr, parent, length):
    # parent:父结点的索引,length:参与调整的数组长度(结点个数)
    # 左孩子的索引
    child = parent \* 2 + 1
    while child < length:
        # 如果右孩子存在,且右孩子大于左孩子
        if child + 1 < length and arr[child + 1] > arr[child]:
            child += 1  # child变成右孩子的索引
        # 父结点的值小于左、右孩子,交换
        if arr[parent] < arr[child]:
            arr[parent], arr[child] = arr[child], arr[parent]
            parent = child
            # 此时,temp和child索引都指向了子结点与父结点交换后,原来的父结点应该插入的位置(原来的子结点的位置)
            # 但是我们不确定这是不是这个结点的最终位置,也就是不确定原来的父元素(小元素)往下调整时会不会破坏下面的大根堆结构

            child = parent \* 2 + 1  # 原来的子结点变成了父结点,再找它的左孩子,如果存在左孩子继续循环,调整根堆
        else:  # 父结点的值大于等于左、右孩子,不交换
            break


def sort(arr):
	# 建堆
    # 从最后一个非终端结点【索引len(arr) // 2 - 1】开始向前遍历到根结点【索引0】
    for i in range(len(arr) // 2 - 1, -1, -1):
        adjust(arr, i, len(arr))
    
	# 每一趟将堆顶元素加入有序子序列(堆顶元素与待排序列中的最后一个元素交换)
    # i:待排序列中最后一个元素的索引
    # 最后一个元素分别是从最后一个结点【索引len(arr) - 1】开始向前遍历到的第二个结点【索引1】
    for i in range(len(arr) - 1, 0, -1):
        # 堆顶和最后一个元素互换位置
        arr[0], arr[i] = arr[i], arr[0]
        # 从顶开始重新调整堆
        adjust(arr, 0, i)
    return arr


if __name__ == "\_\_main\_\_":
    arr = [53, 17, 78, 9, 45, 65, 87, 32]
    print(sort(arr))



![img](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/052da308bd3f401c8b14aa4ed43b1fd8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzM5MTQ5MjgwNjA=:q75.awebp?rk3s=f64ab15b&x-expires=1771315809&x-signature=2KpKJBHDef0ufwVeD9UkWlpJOJE%3D)
![img](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e05762cfd42e42729917efb76a6fdb57~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzM5MTQ5MjgwNjA=:q75.awebp?rk3s=f64ab15b&x-expires=1771315809&x-signature=e7G41%2BDDV%2FHcOesNGOreGbgWqaI%3D)

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化资料的朋友,可以戳这里获取](https://gitee.com/vip204888)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**