这是我参与更文挑战的第13天,活动详情查看: 更文挑战
最近想把自己刷算法题的经验心得整理一下,一方面为了复习巩固,另一方面也希望我的分享能够帮助到更多在学习算法的朋友。
专栏名称叫《算法锦囊》,在讲解算法时会注重整体性,但不会面面俱到,适合有一定算法经验的人阅读。
这一次我们重点来看排序算法,这一部分的所有题目和源码都上传到了github的该目录下,题解主要用Python语言实现。
概述
我们在学习数据结构和算法时,最开始接触的就是排序算法。回想那时候,可能连一个现在看起来很简单的冒泡排序,当时能写出来也会欣喜若狂。
今天我们来回归初心,通过leetcode上的 912. 排序数组,来温习各种排序算法。
冒泡排序
冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求。如果不满足就让它俩互换。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
class Solution:
def bubble_sort(self, nums: List[int]) -> List[int]:
for i in range(len(nums)-1):
for j in range(len(nums)-i-1):
if nums[j] > nums[j+1]:
nums[j], nums[j+1] = nums[j+1], nums[j]
return nums
在传统的冒泡排序的基础上,可以进一步优化,只要某一次遍历时未发生元素的交换,即可认为当前数组已经是顺序的了,可直接退出循环。
class Solution:
def bubble_sort(self, nums: List[int]) -> List[int]:
swapped = True
for i in range(len(nums)-1):
if not swapped:
break
swapped = False
for j in range(len(nums)-i-1):
if nums[j] > nums[j+1]:
nums[j], nums[j+1] = nums[j+1], nums[j]
swapped = True
return nums
插入排序
插入排序的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
class Solution:
def insert_sort(self, nums: List[int]) -> List[int]:
for i in range(1, len(nums)):
if nums[i] < nums[i-1]:
temp = nums[i]
index = i
for j in range(i-1, -1, -1):
if nums[j] > temp:
nums[j+1] = nums[j]
index = j
else:
break
nums[index] = temp
return nums
选择排序
选择排序的原理是,首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
class Solution:
def select_sort(self, nums: List[int]) -> List[int]:
# i 的值对应于已排序值的数量
for i in range(len(nums)):
# 我们假设未排序部分的第一项是最小的
lowest_value_index = i
# 这个循环用来迭代未排序的项
for j in range(i + 1, len(nums)):
if nums[j] < nums[lowest_value_index]:
lowest_value_index = j
# 将未排序元素的最小的值与第一个未排序的元素的值相交换
nums[i], nums[lowest_value_index] = nums[lowest_value_index], nums[i]
return nums
归并排序
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
class Solution:
def merge_sort(self, nums: List[int]) -> List[int]:
def merge(left, right):
res = []
i = 0
j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
res.append(left[i])
i += 1
else:
res.append(right[j])
j += 1
res += left[i:]
res += right[j:]
return res
if len(nums) <= 1:
return nums
mid = len(nums) // 2
# 分
left = self.merge_sort(nums[:mid])
right = self.merge_sort(nums[mid:])
# 合并
return merge(left, right)
快速排序
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
class Solution:
def quick_sort(self, nums):
n = len(nums)
def quick(left, right):
if left >= right:
return nums
pivot = left
i = left
j = right
while i < j:
while i < j and nums[j] > nums[pivot]:
j -= 1
while i < j and nums[i] <= nums[pivot]:
i += 1
nums[i], nums[j] = nums[j], nums[i]
nums[pivot], nums[j] = nums[j], nums[pivot]
quick(left, j - 1)
quick(j + 1, right)
return nums
return quick(0, n - 1)
上面这种写法,因为哨兵每次固定放在左边,特殊情况下,容易使快速排序时间复杂度退化成O(n^2)。
改进版为随机选取。
class Solution:
def quick_sort(self, nums):
n = len(nums)
def quick(left, right):
if left >= right:
return
pivot = random.randint(left, right)
i, j = left, right
nums[left], nums[pivot] = nums[pivot], nums[left]
while i < j:
while i < j and nums[j] > nums[left]:
j -= 1
while i < j and nums[i] <= nums[left]:
i += 1
nums[i], nums[j] = nums[j], nums[i]
nums[left], nums[j] = nums[j], nums[left]
quick(left, j - 1)
quick(j + 1, right)
return
quick(0, n - 1)
return nums
对比归并排序和快速排序。归并排序的处理过程是由下到上的,先处理子问题,然后再合并。而快排正好相反,它的处理过程是由上到下的,先分区,然后再处理子问题。归并排序虽然是稳定的、时间复杂度为 O(nlogn) 的排序算法,但是它是非原地排序算法。归并之所以是非原地排序算法,主要原因是合并函数无法在原地执行。快速排序通过设计巧妙的原地分区函数,可以实现原地排序,解决了归并排序占用太多内存的问题。
堆排序
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
class Solution:
def heapify(self, nums, heap_size, root_index):
# 设最大元素索引为根节点索引
largest = root_index
left_child = (2 * root_index) + 1
right_child = (2 * root_index) + 2
# 如果根节点的左子节点是有效索引,并且元素大于当前最大元素,则更新最大元素
if left_child < heap_size and nums[left_child] > nums[largest]:
largest = left_child
# 对根节点的右子节点执行相同的操作
if right_child < heap_size and nums[right_child] > nums[largest]:
largest = right_child
# 如果最大的元素不再是根元素,则交换它们
if largest != root_index:
nums[root_index], nums[largest] = nums[largest], nums[root_index]
# 调整堆以确保新的根节点元素是最大元素
self.heapify(nums, heap_size, largest)
def heap_sort(self, nums):
n = len(nums)
# 利用列表创建一个最大堆
# range 的第二个参数表示我们将停在索引值为 -1 的元素之前,即列表中的第一个元素
# range 的第三个参数表示我们朝反方向迭代
# 将 i 的值减少1
for i in range(n, -1, -1):
self.heapify(nums, n, i)
# 将最大堆的根元素移动到列表末尾
for i in range(n - 1, 0, -1):
nums[i], nums[0] = nums[0], nums[i]
self.heapify(nums, i, 0)
return nums
其他排序
因篇幅限制,其他一些冷门的排序简单介绍一下,不做代码演示了。
计数排序(Counting Sort)
计数排序要求输入的数据必须是有确定范围的整数。将输入的数据值转化为键存储在额外开辟的数组空间中;然后依次把计数大于 1 的填充回原数组
桶排序(Bucket Sort)
桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
基数排序(Radix Sort)
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。
总结
最后,整理一下各个排序算法的时间复杂度空间复杂度以及稳定性情况。