排序算法是一种通过特定的算法因式将一组或多组数据按照既定模式进行重新排序的方法。通过排序,我们可以得到一个新的序列,该序列遵循一定的规则并展现出一定的规律。经过排序处理后的数据可以更方便地进行筛选和计算,从而大大提高了计算效率。因此,掌握排序算法是每个程序员的基本功之一。
今天我们将详细讲解一些与冒泡排序、快速排序和插入排序相关的leetcode真题。
字如其名,冒泡排序是一种算法,它类似于水中的泡泡逐渐上升,通过逐轮比较和交换,最终使每个元素按照顺序排列。
看一下今天的题目:给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。请注意 ,必须在不复制数组的情况下原地对数组进行操作。进阶:你能尽量减少完成的操作次数吗?
示例 1:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
首先,这道题属于简单题目,一眼基本就可以看出来如何实现。只要遇到零就往后移动,并且冒泡排序最常见的就是双层for循环即可,然后维护两个变量随时进行数据交换。但是这种都知道的解决方案,我们不去实现。我们来实现一下进阶要求,即尽可能减少操作次数来完成数据的转换。
我们可以考虑一下,因为nums数组中不一定只有一个零,所以在操作数据时,我们将所有零都看作一个整体,只移动第一个零即可,其他的零都不用动。下面是图解:
我们在这里写一下相关的Python实现代码。在这段代码中,我们使用了三元表达式的一种变种:(假,真)[条件]
from typing import List
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
i = 0
j = 0
while j < len(nums) :
if nums[j] == 0 :
i = (j,i)[nums[i] == 0]
elif nums[i] == 0 :
nums[i] = nums[j]
nums[j] = 0
i = i + 1
j = j + 1
solution = Solution()
nums = [0,1,0,3,12]
solution.moveZeroes(nums)
print(nums)
快速排序的重要之处在于选择合适的标准数字,通过将大于和小于该数字的元素分成两部分。随后,我们不断重复这个操作。通常情况下,我倾向于使用递归方法,每次分割完后再调用以基准数字为标准的方法,直到只剩下一个元素为止。在今天的例题中,我们将探讨快速排序的应用。
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。
通常情况下,快速排序的时间复杂度是无法达到O(n)的,而且在最坏情况下可能会达到O(n2)的时间复杂度。因此,为了满足题目要求,我们需要对选择排序进行一些改进。
我们先来看下简单实现:
from typing import List
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
return sorted(nums)[len(nums) - k]
solution = Solution()
nums = [3,2,3,1,2,4,5,5,6]
k = 4
print(solution.findKthLargest(nums, k))
然而,这样的实现肯定无法满足O(n)的时间复杂度要求。因此,我们需要进行进一步优化。如果我第一反应是降低时间复杂度,我可能会考虑牺牲空间复杂度,然后逐步进行优化。根据这个,我写出来了递归。
from typing import List
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
def quickSelect(nums,k) -> int:
# 选择一个作为基准
result = nums[0]
# 将数组分为三部分,大于、等于、小于
big ,equal,small = [],[],[]
# for循环不要排序,一进行排序就会增加时间复杂度。
for num in nums:
if num > result:
big.append(num)
elif num < result:
small.append(num)
else :
equal.append(num)
# 说明在big中
if k <= len(big):
return quickSelect(big,k)
if k > len(big) + len(equal):
return quickSelect(small,k - len(big) - len(equal))
return result
return quickSelect(nums,k)
solution = Solution()
nums = [3,2,3,1,2,4,5,5,6]
k = 4
print(solution.findKthLargest(nums, k))
为了更加方便大家理解,我还特意制作了一个简单的图例,以便于更直观地说明。
插入排序(Insertion Sort)是一种简单直观的排序算法。其基本思想是将待排序序列分为已排序和未排序两部分。每次从未排序部分取出一个元素,将其插入到已排序部分的合适位置,直到所有元素都被插入到已排序部分为止。这种排序方法相对其他复杂的排序算法而言,实现简单、容易理解,适用于小规模数据的排序。
我们来看下正题:
给你一个整数数组 nums,请你将该数组升序排列。
输入:nums = [5,1,1,2,0,0]
输出:[0,0,1,1,2,5]
我们先来简单实现一个插入排序:
from typing import List
class Solution:
def insertSort(self,index: int,nums: List[int]):
temp = nums[index]
while index > 0 and temp < nums[index-1] :
nums[index] = nums[index-1]
index = index - 1
nums[index] = temp
def sortArray(self, nums: List[int]) -> List[int]:
for i in range(1,len(nums)):
self.insertSort(i,nums)
return nums
insert = Solution();
nums = [5,1,1,2,0,0]
print(insert.sortArray(nums))
为了更好地说明,我简单地绘制了一个图例。请注意,数字的顺序应根据实际情况而定,而不是根据图中的显示顺序来确定。图中主要展示了交换和比较的过程。
然而,插入排序明显不是最优的算法,因为它的运行时间超过了限制。主要原因是插入排序的时间复杂度仍然偏高。那么,我来提出一种简单的优化方法,主要是减少比较和交换操作的消耗。我们知道,如果数组的前面部分已经是有序的,那么我们可以首先考虑使用二分查找来减少比较次数。我们来实现一下:
from typing import List
class Solution:
def findIndex(self,begin:int, end: int,temp: int,nums: List[int]):
if begin <= end :
return begin
mid = (begin + end ) / 2
if nums[mid] > temp:
findIndex(begin,mid,temp,nums)
else :
findIndex(mid,end,temp,nums)
def insertSort2(self,index: int,nums: List[int]):
temp = nums[index]
small = self.findIndex(0,index,temp,nums)
while index > small and temp < nums[index-1] :
nums[index] = nums[index-1]
index = index - 1
nums[index] = temp
def sortArray(self, nums: List[int]) -> List[int]:
for i in range(1,len(nums)):
self.insertSort2(i,nums)
return nums
insert = Solution();
nums = [5,1,1,2,0,0]
print(insert.sortArray(nums))
本来我觉得这次的任务应该能够顺利通过,但是却没能满足时间限制要求,结果还是超时了。接下来,为了优化交换次数,我需要考虑使用插入排序的高级变体——希尔排序。
希尔排序
希尔排序是一种优化的插入排序算法,它的重点是通过增加间隔来减少元素之间的比较次数。相比于传统的插入排序一次比较一个元素,希尔排序通过间隔的设定,可以一次比较多个元素。为了更好地理解这个过程,我简单地画了一张图。
如果采用之前的插入排序算法,将数字0移动到前面将需要进行多次比较和交换操作,这将导致效率较低。而如果使用希尔排序并增加间隔,可以避免对中间数字进行比较和交换操作,从而有效减少了所需的比较和交换次数。
我们来简单实现一下算法:
from typing import List
class Solution:
def insertSort3(self,index: int,nums: List[int],gap: int):
temp = nums[index]
while index-gap >= 0 and temp < nums[index-gap] :
nums[index] = nums[index-gap]
index = index - gap
nums[index] = temp
def sortArray(self, nums: List[int]) -> List[int]:
gap = len(nums) // 2
while gap > 1 :
for i in range(gap,len(nums)):
self.insertSort3(i,nums,gap)
gap = gap // 2
return nums
insert = Solution();
nums = [5,1,1,2,0,0]
print(insert.sortArray(nums))
终于通过了这道题目,从代码实现上来看,与插入排序相比,它多了一些变量,但仍然很容易理解。然而,由于数组前面不再是绝对有序的,我们需要放弃使用二分查找。