排序-快排、冒泡、插入、选择、归并排序
快排、冒泡、插入、归并排序实现
1. 基础排序
1.1. 快速排序
基本思想:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据小。然后再按此方法对两部分数据分别进行快速排序,整个排序过程 递归 进行,以此达到整个数据变成有序序列。
基于这个思想,可以写出下面的代码:
class Solution: def sortArray(self, nums: List[int]): if not nums or len(nums) == 0: return [] if len(nums) == 1: # 递归终止条件 return nums ref = nums[0] # 基准值 left_part = [] # 保存比基准值小的元素 right_part = [] # 保存比基准值大的元素 for i in range(1, len(nums)): if nums[i] < ref: left_part.append(nums[i]) else: right_part.append(nums[i]) # 对左右两部分分别递归执行该操作 return self.sortArray(left_part)+[ref]+self.sortArray(right_part)
存在问题:通过上述代码发现一个问题,每次递归都重新申请left_part和right_part,这在很大程度上浪费了空间。
优化思路:由于left_part和right_part中存储的内容是nums中的内容,只不过是对nums中的数值进行了分类。因此可以考虑在nums中更新数组位置,从而达到小于基准值的元素存放在nums的前半部分,大于基准值的元素存放在后半部分。如下图:
步骤:
保存数组第一个元素在ref中,同时定义left和right指针。
- nums[right]=4>ref,表示当前元素大于基准值,此时直接向前移动right,不用改变元素位置。
- nums[right]=5>ref,此时直接向前移动right,不用改变元素位置
- nums[right]=1<ref, 此时right指向的元素小于基准值,此时应该放在nums数组的前面部分,因此与将right指针指向的元素存放在left指向的位置(由于left指向的元素是已经保存在了ref中,所以不用担心被覆盖),同时left向后移动。
- nums[left]=2<ref, 此时表示当前元素小于ref,就应该放在nums的前面位置,因此不用更新元素位置,直接left指向下一个位置即可。
说明:
- 首先,比较right指向的位置与ref的大小,如果nums[right]>ref,此时就right前移,然后继续比较即可。
- 如果nums[right]<ref,就将right之前的元素与left指向的元素交换位置,然后开始比较left指向位置的元素与ref的大小。
- 函数中定义的flag是为了控制移动的指针,当flag=True的时候,比较right指向的元素。当flag=left的时候,比较left指向的元素。
class Solution: def sortArray(self, nums): if not nums or len(nums) == 0: return [] if len(nums) == 1: # 递归终止条件 return nums ref = nums[0] # 基准值 left = 0 right = len(nums)-1 flag = True while left < right: if flag: if nums[right] < ref: nums[left] = nums[right] left += 1 flag = False else: right -= 1 else: if nums[left] > ref: nums[right] = nums[left] right -= 1 flag = True else: left += 1 return self.sortArray(nums[:left])+[ref]+self.sortArray(nums[left+1:])
1.2. 冒泡排序
基本思想:重复走访要排序的数组,依次比较两个相邻的元素,如果顺序错误就交换,知道没有相邻元素需要交换,也就是说序列已经排序完成。
class Solution: def sortArray(self, nums): count = 0 # 记录遍历次数 while True: flag = False # 每次遍历是否交换元素 for i in range(len(nums)-1-count): # 每遍历一次都可以确定nums最后一个元素是最大值,因此就不用比较最后一个元素,所以可以减去count j = i + 1 if nums[i] > nums[j]: nums[j], nums[i] = nums[i], nums[j] flag = True count += 1 if not flag: break return nums
1.3. 插入排序
基本思想:对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
思路如下图:
说明:
- current表示当前要排序的元素
- pre_index表示已排序元素的末尾元素的索引,从而能够遍历已排序数组,从而找到合适的插入位置。
class Solution: def sortArray(self, nums): for i in range(len(nums)): current = nums[i] # 当前元素 pre_index = i - 1 while pre_index >= 0 and current < nums[pre_index]: # 移动数组元素,从而在合适的位置插入current nums[pre_index+1] = nums[pre_index] pre_index -= 1 nums[pre_index+1] = current return nums
1.4. 选择排序
基本思想:第一次从待排序的数据元素中选出最小的元素,存放在序列的起始位置,然后再从剩余未排序的元素中寻找最小元素,放到已排序的序列的末尾,以此类推,知道全部待排序的数据元素的个数为零。
图形化过程如下图:
- right指向起始位置,right右侧表示未排序序列(包含right指向元素)
- 从right右侧选择最小元素1,然后存放在起始位置。right右移动一位。
- 从right右侧选择最小元素2,然后存放到已排序序列的后面。right右移一位。
- 重复过程,直到right移动到数组末尾即可。
class Solution: def sortArray(self, nums): # right指向未排序数组的起始元素 right = 0 for _ in range(len(nums)): min_num = float('inf') # 保存最小值 min_num_i = -1 # 保存最小值索引 for i in range(right, len(nums)): if nums[i] < min_num: min_num = nums[i] min_num_i = i nums[right], nums[min_num_i] = nums[min_num_i], nums[right] right += 1 return nums
1.5. 归并排序
基本思想:归并排序是建立在归并操作上的一种有效的排序算法,该算法采用经典的分治策略。
思路:先分再合。如下代码所示:
- 通过递归方式在sortArray中不断的切分数组。
- 在merge中合并数组
class Solution: def sortArray(self, arr): if len(arr) <= 1: # 递归终止条件,数组长度小于1 return arr # 数组切分 import math mid = math.floor(len(arr)/2) left = arr[:mid] right = arr[mid:] # 合并 return self.merge(self.sortArray(left), self.sortArray(right)) def merge(self, left, right): """ 合并操作,类似于2个有序链表合并为1个有序链表的思路 """ result = [] # 保存合并之后的有序数组 while left and right: if left[0] < right[0]: result.append(left.pop(0)) else: result.append(right.pop(0)) if left: # 如果执行到这里,表示right里面的数据已经全部添加到result里面去了, # 这里直接把left中剩余元素添加到result中即可 result.extend(left) if right: result.extend(right) return result