LT专项【排序方法】

五种比较流行的排序方法

(1) 冒泡排序

对数组N长度的每一对相邻元素比较,如果比较第一个比第二个大,即交换。
做完一轮交换,那么最大元素会落在数组的最后一个元素。
新的一轮是 除去最后一个元素后的数组N-1进行同样的处理。
【这样的时间复杂度是O(N^2), 实际上是用这种方法经常会超出时间限制】
Python Code

def bubbleSort(arr):
	L = len(arr)
	for i in range(1, L):
		for j in range(0,L-i):
			if arr[j] > arr[j+1]:
				arr[j], arr[j+1] = arr[j+1], arr[j]
	return arr

(2) 选择排序

第一轮:
对于数组N,初始化第一个元素作为最小值,然后对后面的数进行比较,遇到遇到比第一个元素小的值,则更换最小值,继续往后比较,如果还有更小的,则更换最小值,一轮过后,就能找到数组的最小值了,然后跟第一个元素交换。
第二轮:
这个时候,第一个元素是最小值,现在除去第一个元素。对数组N-1重复前面的操作

def selectSort(arr):
	##special cases 
	if arr is None or len(arr)==1: return arr
	L = len(arr)
	for i in range(L-1):
		#每一轮都是寻找最小值
		#先set min index 
		min_index = i 
		for j in range(i+1, L):
			if arr[j] < arr[min_index]: 
				min_index = j 
		##循环一轮之后,把最小值换到前面
		if min_index != i: 
		##与最小值交换
			arr[i], arr[min_index] = arr[min_index] , arr[i]
	return arr 

同样时间复杂度是O(logN^2) 一般都会超出时间限制

(3) 快速排序

重点来了 快速排序比较常用。是最实用的排序算法,没有之一,各大语言标准库的排序函数也基本都是基于快排实现的。
把最后一个元素或者是第一个元素作为基准pivot,对前N-1个数进行比较,如果比基准小的 放在左边,大的放在右边
也就是 将未排序的序列排序成 独立的两个部分,其中左边部分序列都比划分值小,右边部分的序列比划分值大,此时划分值的位置已确认,然后再对这两个序列按照同样的方法进行排序。

def partition(arr,low,high):
	i = low -1 
	pivot  = arr[high] 
	for j in range(low,high):
		if arr[j]<= pivot:
			i +=1 
			arr[i],arr[j] = arr[j],arr[i]
	arr[i+1],arr[high] = arr[high],arr[i+1]
	return i + 1 
def quickSort(arr,low,high):
	if low < high:
		mid = partition(arr,0,len(arr)-1)
		quickSort(arr,low,mid-1)
		quickSort(arr,mid+1,high)

关于快速排序的最好、最坏、平均 时间复杂度分析
快速排序的方法是用第一个数或者是最后一个数作为基准pivot,然后对arr数组遍历。用 i 来代表排序后比pivot 小的最后位置。最后返回i +1 代表划分为两个区间的位置。
(1)现在分析最优情况是:
每次找到的pivot刚好就能把数组arr 平均分成两半。两边的树平衡。
如果数组长度为N,那么根据平衡的递归树的定义则 树的深度D = log(N) + 1
最优情况
第一轮:
第一轮是遍历了所有的数,所以这个时间复杂度是O(N), 下一轮就是对左右两个区间(假设两个区间长度是N/2,因为最优情况)进行搜索
所以,总时间复杂度T(N) = 2* T(N/2) + N

第二轮:
同理针对 T(N/2) 我们继续也是遍历N/2个数 所以 T(N/2) = 2* T(N/4) + N/2
总时间复杂度T(N) = 2* T(N/2) + N = 2*(2* T(N/4) + N/2 ) + N = 4T(N/4) + 2N
T ( N ) = 2 2 T ( N 2 2 ) + 2 N T(N) = 2^2 T(\frac{N}{2^2}) + 2N T(N)=22T(22N)+2N

同理针对第三轮也是
T(N) = 2(2( 2T(N/8) + N/4) + N/2) + N = 8T(N/8) + 3N
T ( N ) = 2 3 T ( N 2 3 ) + 3 N T(N) = 2^3 T(\frac{N}{2^3}) + 3N T(N)=23T(23N)+3N

第K轮:
T ( N ) = 2 K T ( N 2 K ) + K N T(N) = 2^K T(\frac{N}{2^K} )+ KN T(N)=2KT(2KN)+KN
由于我们知道树的深度是 log(N) ,即K= log(N),带入公式就是

T ( N ) = 2 l o g ( N ) T ( N 2 l o g ( N ) ) + l o g ( N ) N T(N) = 2^{log(N)} T(\frac{N}{2^{log(N)}} )+ {log(N)}N T(N)=2log(N)T(2log(N)N)+log(N)N

由于 2 l o g ( N ) = N 2^{log(N)} = N 2log(N)=N, 所以

T ( N ) = N T ( N N ) + l o g ( N ) N = N T ( 1 ) + N l o g N = N + N l o g N T(N) =NT(\frac{N}{N}) + log(N)N = NT(1) + NlogN =N+ NlogN T(N)=NT(NN)+log(N)N=NT(1)+NlogN=N+NlogN

所以时间复杂度是 O(NlogN)

(2)最坏的情况是 数组是正序或者倒序
当数组是正序或者倒序,每次划分只得到一个比上一次划分少一个记录的子序列,另一个子序列为空,所以组成的递归树是一颗偏斜的树,需要执行n‐1次递归调用。所以有:
N + (N-1) + (N-2) + (N-3) +, + 1 = (N+1)N/2
所以时间复杂度是O(N^2)
【PS:平均时间复杂度可以当作是 一次坏的划分,接着是一次好的划分,轮着来,最后的时间复杂度也是O(nlogn).】

针对空间复杂度是:
快排的空间复杂度是O(logn),因为快排的实现是递归调用的, 而且每次函数调用中只使用了常数的空间,因此空间复杂度等于递归深度O(logn)

(4) 并归排序

先把 nums的所有元素分开 一半,然后一直分,分成一个个单独的[num],再两两排序合并
具体怎么合并呢?用双指针来做 p、q放在两个数组第一位,然后谁小放入ans里面。
归并排序采用分而治之的原理:
(1)将一个序列从中间位置分成两个序列;
(2)再这两个子序列按照第一步继续二分下去;
(3)直到所有子序列的长度都为1,也就是不可以再二分截止。这时候再两两合并成一个有序序列即可。
请添加图片描述
所以分两步,一步是分开,一步是合并。
(1)递归地分开

def split(nums):
	mid = len(nums)//2
	left_seq = nums[:mid]
	right_seq = nums[mid:]
	left = split(left_seq) ##对左区间递归 不断地分开
	right = split(right_seq) ##对右区间递归 不断地分开

(2)对任意两个区间 进行合并

def merge(left, right):
	ans = []
	p,q =0,0
	while p<len(left) and q<len(right):
		if left[p] < right[q]:
			ans.append(left[p])
			p+=1
		else:
			ans.append(right[q])
			q+=1 
	ans += left[p:]
	ans += right[q:]
	return ans 

将两个部分合并在一起,就是并归排序

def merge(left,right):
	#合并两个有序数组
	ans = []
	p,q =0,0 
	while p < len(left) and q < len(right):
		if left[p] < right[q]:
			ans.append(left[p])
			p+=1 
		else: 
			ans.append(right[q])
			q+=1 
			
	ans += left[p:]
	ans += right[q:]
	return ans 
def mergeSort(nums):
	if len(nums)<=1:return nums
	#分成两半
	mid = len(nums)//2
	left_seq = nums[:mid]
	right_seq = nums[mid:]
	left = mergeSort(left_seq)
	right = mergeSort(right_seq)
	return merge(left,right)

时间复杂度和空间复杂度
假设nums长度为N
(1)递归部分
递归的第一层是 分成 两个区间 长度分别为N/2
递归的第二层将分成 4个区间长度为N/4
递归的第三层分成8个区间,长度为N/8

结束时是
递归的第K层分成(2^K )个区间,长度为N/(2^K)

因为结束的时候,长度是1,所以N/(2^K) = 1
所以,K = logN,即递归了logN层

(2)合并部分
倒着合并,
即在第logN层的时候,每个子区间长度都是1 , 对每相邻的子区间合并。n个数字都会被遍历一次。所以这一层的时间复杂度是 O(N)


轮到第二层的时候,每个区间的长度为 N/4 总共有4个区间,总共合并两次,n个数字都会被遍历。所以这一层的时间复杂度也是 O(N)
第一层也是一样的道理
所以 合并的时候 每一层都会对n个元素操作,每一层的时间复杂度都是 O(N)。总共有logN层
则总共的时间复杂度是 O(NlogN)

在合并子列时需要申请临时空间,而且空间大小随数列的大小而变化,所以空间复杂度为O(n)

(5) 堆排序

最大堆:最大堆中的最大元素在根结点(堆顶);堆中每个父节点的元素值都大于等于其子结点(如果子节点存在)

最小堆:最小堆中的最小元素出现在根结点(堆顶);堆中每个父节点的元素值都小于等于其子结点(如果子节点存在)

堆排序的过程可以具体分为三步,创建堆,调整堆,堆排序

核心是 先创建一个无序的堆,然后如果父节点为 i,
则它的左节点位置是 2i + 1 ; 右节点是2i + 2;
所以 要比较 把其中数值最大放在父节点上。不断的重复,等到最大的数放在根那里,就可以进行交换,放在末尾端。
不断重复上面的操作。

code:
对父节点以及对应的左右节点:做比较并进行交换把最大值放在父节点上。

def heapify(arr, size, rootIndex):
	##子节点
	left = 2*rootIndex + 1 
	right = 2*rootIndex + 2 
	maxIndex = rootIndex 
	if left < size and arr[maxIndex] < arr[left]: maxIndex = left 
	if right < size and arr[maxIndex] < arr[right]: maxIndex = right
	if maxIndex != rootIndex: 
		arr[maxIndex], arr[rootIndex] = arr[rootIndex], arr[maxIndex]
		##继续递进
		heapify(arr, size, maxIndex)

def heapSort(arr):
	##从最后一个节点开始
	for i in range(len(arr)-1, -1,-1):
		heapify(arr, len(arr), i)
	##上面code建好了最大堆
	##下面是进行交换 把建立好的最大堆的根节点 也就是最大值 放在列表的末端
	##然后重复
	for i in range(len(arr)-1,-1,-1):
		arr[i], arr[0]=arr[0],arr[i]
		#互换之后,堆中就少了一个元素,所以当前堆的个数变了,size变为i
		#再建堆
		heapify(arr,i,0)
	return arr

时间复杂度都是nlogn,但不稳定
【PS:补充一个点 内置函数sort的时间复杂度是nlogn; min和max的时间复杂度是n】

涉及的排序题目

LT215. 数组中的第K个最大元素

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素

输入: [3,2,1,5,6,4] 和 k = 2
输出: 5
输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4

这道是经典的先排序再返回倒数第K个数。
方法一:快速排序
快速排序的特别是partition返回的位置mid,左边有mid个数是比nums[mid]小,右边有L -(mid+1)个数比nums[mid]大。现在要返回倒数第K个数,则如果L-mid=k的话,恰好是返回nums[L-mid],即是返回倒数第K个数。这个便是返回条件。

class Solution:
    def partition(self, nums, low, high):
        #pivot 
        pivot = nums[high]
        i= low-1 
        for j in range(low, high):
            if nums[j] <= pivot:
                i+=1 
                nums[i], nums[j] = nums[j], nums[i]
        nums[i+1], nums[high]=nums[high], nums[i+1]
        return i+1 
    def topK(self, nums, low, high, k):
        L = len(nums) 
        if low < high:
            mid = self.partition(nums, low, high)
            if L-mid == k: return 
            elif L-mid > k: 
            ##说明ans在右区间,即[mid+1, high]
                self.topK(nums, mid+1, high, k)
            else: 
                self.topK(nums, low, mid-1, k)
    
    def findKthLargest(self, nums: List[int], k: int) -> int:
        low = 0
        high = len(nums)-1
        self.topK(nums, low, high, k)
        return nums[-k]	

方法二:并归排序
只要记得堆排序的code,最后返回nums[-k]即可

class Solution: 
    def merge(self, nums1, nums2):
        ans = []
        p,q =0,0
        while p < len(nums1) and q < len(nums2):
            if nums1[p] < nums2[q]:
                ans.append(nums1[p])
                p+=1 
            else:
                ans.append(nums2[q])
                q+=1 
        ans += nums1[p:]
        ans += nums2[q:]
        return ans 
    def mergeSort(self, nums):
        if len(nums) <= 1: return nums
        mid = len(nums)//2
        left_seq = nums[:mid]
        right_seq = nums[mid:]
        left = self.mergeSort(left_seq)
        right = self.mergeSort(right_seq)
        return self.merge(left, right)
       
    def findKthLargest(self, nums: List[int], k: int) -> int:
        ans = self.mergeSort(nums)
        return ans[-k]

方法三:堆排序
只要记得堆排序的code,最后返回nums[-k]即可

class Solution: 
    def heapify(self, nums, size, rootIndex): 
        #子节点
        left = 2*rootIndex + 1 
        right = 2*rootIndex + 2
        #找到最大值的index
        maxIndex = rootIndex
        if left < size and nums[left] > nums[maxIndex]: maxIndex=left 
        if right < size and nums[right] > nums[maxIndex]: maxIndex = right 
        #置换
        if maxIndex != rootIndex: 
            nums[maxIndex], nums[rootIndex] = nums[rootIndex], nums[maxIndex]
            #继续递进
            self.heapify(nums, size, maxIndex)
    def findKthLargest(self, nums: List[int], k: int) -> int:
        ##堆方法
        #从最后一个节点出发
        L = len(nums)
        for i in range(L-1, -1,-1):
            self.heapify(nums, L, i)

        for i in range(L-1,  -1,-1):
            nums[i], nums[0] = nums[0], nums[i]
            self.heapify(nums, i, 0)
        return nums[-k]

LT4. 寻找两个正序数组的中位数

给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n))

输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
输入:nums1 = [], nums2 = [1]
输出:1.00000

这道题咋眼一看,以为使用快速排序、并归排序、堆排序都可以解决。但无论哪种方法,时间复杂度都是(m+n)*log(m+n) 不符合题目要求的时间复杂度。
方法一:
只使用并归排序的merge,时间复杂度是max(len(nums1), len(nums2)),应该比要求的时间复杂度小。

class Solution:
    def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
        p,q = 0,0
        ans = []
        while p < len(nums1) and q < len(nums2):
            if nums1[p] < nums2[q]:
                ans.append(nums1[p])
                p+=1 
            else:
                ans.append(nums2[q])
                q+=1 
        ans+= nums1[p:]
        ans+= nums2[q:]
        L = len(ans)
        if L%2==1:
            return float(ans[L//2])
        else:
            return float((ans[L//2]+ans[L//2 -1 ])/2)

方法二:【特殊的二分法】

class Solution:
    def getKth(self,nums1,nums2,k):
        N1 = len(nums1)
        N2 = len(nums2)
        #p,q的作用是分别记录 两个数组的chi始坐标
        p,q = 0,0
        while True:
            if p == N1: return float(nums2[q+k-1])
            if q == N2: return float(nums1[p+k-1])
            if k ==1: return float(min(nums1[p], nums2[q]))

            index1 = min(p+k//2-1, N1-1)
            index2 = min(q+k//2-1, N2-1)
            if nums1[index1] <= nums2[index2]:       
                k = k - index1 + p - 1
                p = index1+1 
            else:
                k = k - index2 + q - 1
                q = index2 + 1 


    def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
        N1 = len(nums1)
        N2 = len(nums2)
        N = N1 + N2
        k = N//2
        if N%2==1: 
            return self.getKth(nums1,nums2,(N+1)//2)
        else: 
            ans = self.getKth(nums1,nums2,k) + self.getKth(nums1,nums2,k+1)
            return ans/2

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jianafeng

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值