时间复杂度

数据结构 - 图1

大O标记法 计算10个元素 计算100个元素 计算1000个元素
O(1) 1 1 1
O(log N) 3 6 9
O(N) 10 100 1000
O(N log N) 30 600 9000
O(N^2) 100 10000 1000000
O(2^N) 1024 1.26e+29 1.07e+301
O(N!) 3628800 9.3e+157 4.02e+2567

O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)

空间复杂度

顺序表(数组)

数组元素在内存中存放是连续的,每个元素占用的内存相同。数组可以通过下标迅速访问数组中的任何元素O(1)。但是在数组中增加一个元素时,需要移动大量元素,在内存中空出一个元素的空间,然后将要增加的元素放进其中O(n)。同理,要删除一个元素,需要移动大量元素来填掉被移动的元素O(n)。
如果需要快速访问数据,很少插入和删除元素,使用数组。

链表

链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是不像数组一样连续存储数据,而是在每一个节点(数据存储单元)里存放下一个节点的位置信息(内存地址)。
数据结构 - 图2

数组和链表的区别

  • 存储位置:数组将元素在内存中连续存放,而链表中的元素在内存中不是顺序存储的。
  • 存储空间:一般情况下,存储相同多的数据,数组占用的内存较少,而链表还需要存储前驱和后继,需要更多的内存。
  • 长度的可变性:数组的长度在定义的时候给定,如果存储的数据个数超过数组的初始长度,会发生溢出,而链表的长度可以按实际情况伸缩。
  • 按序号查找:按序号查找时,数组可以随机访问,时间复杂度为O(1),而链表不支持随机访问,平均时间复杂度为O(n)。
  • 按值查找:如果数组是无序的,数组和链表的时间复杂度均为O(n),如果数组是有序的,使用二分查找可以使时间复杂度降低到O(logn)。
  • 插入和删除:数组平均需要移动n/2个元素,而链表只需要修改指针。

哈希表可以结合数组和链表的优点


数组 链表
读取 O(1) O(n)
插入 O(n) O(1)
删除 O(n) O(1)

链表有三种类型(单向链表、单向循环链表、双面链表)

  1. class SingleNode:
  2. """单链表的结点"""
  3. def __init__(self,item):
  4. # item存放数据元素
  5. self.item = item
  6. # next是下一个节点的标识
  7. self.next = None
  8. class SingleLinkList:
  9. """单链表"""
  10. def __init__(self):
  11. self._head = None
  12. def is_empty(self):
  13. """判断链表是否为空"""
  14. return self._head == None
  15. def length(self):
  16. """链表长度"""
  17. # cur初始时指向头节点
  18. cur = self._head
  19. count = 0
  20. # 当到达尾部时,尾节点指向None
  21. while cur != None:
  22. count += 1
  23. # 将cur后移一个节点
  24. cur = cur.next
  25. return count
  26. def travel(self):
  27. """遍历链表"""
  28. cur = self._head
  29. while cur != None:
  30. print(cur.item, end=" ")
  31. cur = cur.next
  32. def add(self, item):
  33. """头部添加元素"""
  34. # 先创建一个保存item值的空节点
  35. node = SingleNode(item)
  36. # 将新节点的链接域next指向头节点,即_head指向的位置
  37. node.next = self._head
  38. # 将链表的头_head指向新节点
  39. self._head = node
  40. def append(self, item):
  41. """尾部添加元素"""
  42. node = SingleNode(item)
  43. # 先判断链表是否为空,若是空链表,则将_head指向新节点
  44. if self.is_empty():
  45. self._head = node
  46. # 若不为空,则找到尾部,将尾节点的next指向新节点
  47. else:
  48. cur = self._head
  49. while cur.next != None:
  50. cur = cur.next
  51. cur.next = node
  52. def insert(self, pos, item):
  53. """指定位置添加元素"""
  54. # 若指定位置pos为第一个元素之前,则执行头部插入
  55. if pos <= 0:
  56. self.add(item)
  57. # 若指定位置超过链表尾部,则执行尾部插入
  58. elif pos > (self.length()-1):
  59. self.append(item)
  60. # 找到指定位置
  61. else:
  62. node = SingleNode(item)
  63. count = 0
  64. # pre用来指向指定位置pos的前一个位置pos-1,初始从头节点开始移动到指定位置
  65. pre = self._head
  66. while count < (pos-1):
  67. count += 1
  68. pre = pre.next
  69. # 先将新节点node的next指向插入位置的节点
  70. node.next = pre.next
  71. # 将插入位置的前一个节点的next指向新节点
  72. pre.next = node
  73. def remove(self,item):
  74. """删除节点"""
  75. cur = self._head
  76. pre = None
  77. while cur != None:
  78. # 找到了指定元素
  79. if cur.item == item:
  80. # 如果第一个就是删除的节点
  81. if cur == self._head:
  82. # 将头指针指向头节点的后一个节点
  83. self._head = cur.next
  84. else:
  85. # 将删除位置前一个节点的next指向删除位置的后一个节点
  86. pre.next = cur.next
  87. break
  88. else:
  89. # 继续按链表后移节点
  90. pre = cur
  91. cur = cur.next
  92. def search(self,item):
  93. """链表查找节点是否存在,并返回True或者False"""
  94. cur = self._head
  95. while cur != None:
  96. if cur.item == item:
  97. return True
  98. cur = cur.next
  99. return False
  100. # 测试代码
  101. if __name__ == "__main__":
  102. ll = SingleLinkList()
  103. ll.add(1)
  104. ll.add(2)
  105. ll.append(3)
  106. ll.insert(2, 4)
  107. print("length:",ll.length())
  108. ll.travel()
  109. print(ll.search(3))
  110. print(ll.search(5))
  111. ll.remove(1)
  112. print("length:",ll.length())
  113. ll.travel()

双向链表

一种更复杂的链表是“双向链表”或“双面链表”。每个节点有两个链接:一个指向前一个节点,当此节点为第一个节点时,指向空值;而另一个指向下一个节点,当此节点为最后一个节点时,指向空值。
数据结构 - 图3
链表中的元素在内存中不是顺序存储的,它们通过元素中的指针联系起来。每个元素包括两个部分,一个是存储的数据,一个是指向下一个元素的指针。
如果需要访问链表中的一个元素,需要从第一个元素开始,一直找到需要的元素位置。
但是增加和删除一个元素就非常简单,只需要改变元素中的指针。
如果经常需要插入和删除元素,使用链表。

栈(先入后出)

栈(stack),有些地方称为堆栈,是一种容器,可存入数据元素、访问元素、删除元素,它的特点在于只能允许在容器的一端(称为栈顶端指标,英语:top)进行加入数据(英语:push)和输出数据(英语:pop)的运算。没有了位置概念,保证任何时候可以访问、删除的元素都是此前最后存入的那个元素,确定了一种默认的访问顺序。
由于栈数据结构只允许在一端进行操作,因而按照后进先出(LIFO, Last In First Out)的原理运作。
数据结构 - 图4

  1. class Stack(object):
  2. """栈"""
  3. def __init__(self):
  4. self.items = []
  5. def is_empty(self):
  6. """判断是否为空"""
  7. return self.items == []
  8. def push(self, item):
  9. """加入元素"""
  10. self.items.append(item)
  11. def pop(self):
  12. """弹出元素"""
  13. return self.items.pop()
  14. def peek(self):
  15. """返回栈顶元素"""
  16. return self.items[len(self.items)-1]
  17. def size(self):
  18. """返回栈的大小"""
  19. return len(self.items)
  20. if __name__ == "__main__":
  21. stack = Stack()
  22. stack.push("hello")
  23. stack.push("world")
  24. stack.push("itcast")
  25. print(stack.size())
  26. print(stack.peek())
  27. print(stack.pop())
  28. print(stack.pop())
  29. print(stack.pop())

队列(先入先出)

队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
队列是一种先进先出的(First In First Out)的线性表,简称FIFO。允许插入的一端为队尾,允许删除的一端为队头。队列不允许在中间部位进行操作!假设队列是q=(a1,a2,……,an),那么a1就是队头元素,而an是队尾元素。这样我们就可以删除时,总是从a1开始,而插入时,总是在队列最后。这也比较符合我们通常生活中的习惯,排在第一个的优先出列,最后来的当然排在队伍最后。

有两种类型(单端队列、双端队列)
数据结构 - 图5
数据结构 - 图6

双端队列

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

是一种特别的二叉树,满足以下条件的二叉树,可以称之为

  1. 完全二叉树
  2. 每一个节点的值都必须 大于等于或者小于等于 其孩子节点的值

特点:

  1. 可以在 O(logN) 的时间复杂度内向 中插入元素
  2. 可以在 O(logN) 的时间复杂度内向 中删除元素
  3. 可以在 O(1) 的时间复杂度内获取 中的最大值或最小值

堆的分类

  • 最大堆:堆中每一个节点的值 都大于等于 其孩子节点的值。所以最大堆的特性是 堆顶元素(根节点)是堆中的最大值
  • 最小堆:堆中每一个节点的值 都小于等于 其孩子节点的值。所以最小堆的特性是 堆顶元素(根节点)是堆中的最小值

数据结构 - 图8

  1. import heapq
  2. nums = [2, 43, 45, 23, 12]
  3. # 创建堆,会生成一个从小到大排列的数组,Py默认都是最小堆,如果想用最大堆,可以自己加负号
  4. heapq.heapify(nums)
  5. [2, 12, 23, 45, 43]
  6. # 弹出堆顶元素,也就是头部
  7. print(heapq.heappop(nums))
  8. 2
  9. # 加入堆
  10. heapq.heappush(nums, 3)
  11. # 查询最大的1个值,这个数字可以自定义,此操作不会弹出元素
  12. print(heapq.nlargest(1, nums))
  13. [45]
  14. # 查询最小的1个值,这个数字可以自定义,此操作不会弹出元素
  15. print(heapq.nsmallest(1, nums))
  16. [3]
  17. # 弹出所有值
  18. print([heapq.heappop(nums) for _ in range(len(nums))])
  19. [2, 12, 23, 43, 45]

TopK堆解法

  1. class Solution(object):
  2. def findKthLargest(self, nums, k):
  3. # 这里使用了自建堆,可以使用[0]等索引方法
  4. heap = [x for x in nums[:k]]
  5. heapq.heapify(heap)
  6. for i in range(k, len(nums)):
  7. if nums[i] > heap[0]:
  8. heapq.heappop(heap)
  9. heapq.heappush(heap, nums[i])
  10. return heap[0]
  11. # 另一种方法,常用
  12. heap = []
  13. for i in nums:
  14. heapq.heappush(heap, i)
  15. if len(heap) > k:
  16. heapq.heappop(heap)
  17. return heap[0]

建堆的时间复杂度

建堆的方式有两种,比较常见的建堆方式是自底向上,因为时间复杂度更低,为O(N)

  1. 自顶向下的建堆方式,这个方式是从根节点开始,然后一个个的插入堆的末尾,向上进行调整。该建堆方式的时间复杂度为 O(logN)
  2. 自底向上的建堆方式,这个方式是从倒数第二层的节点(叶子节点的上一层)开始,从右向左,从下到上的向下进行调整。 O(N)