各种排序算法

常用的排序算法

以下是一些最基本的排序算法。虽然在python/ C++ 里可以通过 std::sort() 快速排序,而且刷题
时很少需要自己手写排序算法,但是熟习各种排序算法可以加深自己对算法的基本理解,以及解
出由这些排序算法引申出来的题目。

1.选择排序(Selection Sort)

先从 n 个数字中找到最小值 min1,如果最小值 min1 的位置不在数组的最左端(也就是 min1 不等于 arr[0]),则将最小值 min1 和 arr[0] 交换,接着在剩下的 n-1 个数字中找到最小值 min2,如果最小值min2 不等于 arr[1],则交换这两个数字,依次类推,直到数组 arr 有序排列。算法的时间复杂度为 O ( n 2 ) 。 O(n^2)。 O(n2)

选择排序算法的原理如下:

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
  2. ​再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
  3. 重复第二步,直到所有元素均排序完毕。
def selectSort(nums):
	n = len(nums)
	for i in range(n-1):
		min_idx = i
		for j in range(i+1,n):
			if nums[j]<nums[min_idx]:
				min_idx = j
		nums[i],nums[min_idx] = nums[min_idx],nums[i]
	return nums

2.冒泡排序(Bubble Sort)

首先从数组的第一个元素开始到数组最后一个元素为止,对数组中相邻的两个元素进行比较,如果位于数组左端的元素大于数组右端的元素,则交换这两个元素在数组中的位置。这样操作后数组最右端的元素即为该数组中所有元素的最大值。接着对该数组除最右端的 n-1 个元素进行同样的操作,再接着对剩下的 n-2 个元素做同样的操作,直到整个数组有序排列。算法的时间复杂度为 O ( n 2 ) 。 O(n^2)。 O(n2)

冒泡排序算法的原理如下:

  • 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  • 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
  • 针对所有的元素重复以上的步骤,除了最后一个
  • 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
def bubbleSort(nums):
	n = len(nums)
	for i in  range(n):
            for j in range(0,n-i-1):
                if nums[j]>nums[j+1]:
                    nums[j],nums[j+1] = nums[j+1],nums[j]
    return nums

3.插入排序(Insert Sort)

它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。例如要将数组 arr=[4,2,8,0,5,1] 排序,可以将4看做是一个有序序列,将 [2,8,0,5,1] 看做一个无序序列。无序序列中 2 比 4 小,于是将 2 插入到 4 的左边,此时有序序列变成了 [2,4],无序序列变成了 [8,0,5,1]。无序序列中 8 比 4 大,于是将 8 插入到 4 的右边,有序序列变成了 [2,4,8],无序序列变成了 [0,5,1]。以此类推,最终数组按照从小到大排序。该算法的时间复杂度为 O ( n 2 ) 。 O(n^2)。 O(n2)

插入排序算法的原理如下:

  1. 从第一个元素开始,该元素可以认为已经被排序;
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  4. 重复步骤 3,直到找到已排序的元素小于或者等于新元素的位置;
  5. 将新元素插入到该位置后;
  6. 重复步骤 2~5。
def insertSort(nums):
        n = len(s)
        for i in range(1,n):
            j = i
            while j>0:
                if nums[j]<nums[j-1]:
                    nums[j],nums[j-1] = nums[j-1],nums[j]
                j-=1
        return nums

4.希尔排序(Shell Sort)

希尔排序(Shell’s Sort)在插入排序算法的基础上进行了改进,算法的时间复杂度与前面几种算法相比有较大的改进,但希尔排序是非稳定排序算法
其算法的基本思想是:先将待排记录序列分割成为若干子序列分别进行插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行一次直接插入排序。该算法时间复杂度为 O ( n l o g n ) 。 O(n log n)。 O(nlogn)

希尔排序算法的原理如下:

  1. 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
  2. 按增量序列个数 k,对序列进行 k 趟排序;
  3. 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

例如,假设有这样一组数[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ],如果我们以步长为5开始进行排序,我们可以通过将这列表放在有5列的表中来更好地描述算法,这样他们就应该看起来是这样(竖着的元素是步长组成):

13 14 94 33 82
25 59 94 65 23
45 27 73 25 39
10

然后我们对每列进行排序:

10 14 73 25 23
13 27 94 33 39
25 59 94 65 82
45

将上述四行数字,依序接在一起时我们得到:[ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ]。这时10已经移至正确位置了,然后再以3为步长进行排序:

10 14 73
25 23 13
27 94 33
39 25 59
94 65 82
45

排序之后变为:

10 14 13
25 23 33
27 25 59
39 65 73
45 94 82
94

最后以1为步长就是简单的插入排序了

def shellSort(nums):
    n = len(nums)
    gap = n//2
    # gap变化到0之前,插入排序执行的次数
    while gap >= 1:
    	# 按步长进行插入排序、与普通的插入排序的区别就是步长
        for i in range(gap,n):  
            j = i
            # 插入排序
            while j>0:
                if nums[j]<nums[j-gap]:
                    nums[j],nums[j-gap] = nums[j-gap],nums[j]
                    j -= gap
                else:
                    break
        # 缩短步长
        gap//=2
	return nums

5.归并排序(Merge Sort)

归并排序是采用分治法的一个非常典型的应用。
归并排序的思想就是先递归分解数组,再合并数组。将数组分解最小之后,然后合并两个有序数组,基本思路是比较两个数组的最前面的数,谁小就先取谁,取了后相应的指针就往后移一位。然后再比较,直至一个数组为空,最后把另一个数组的剩余部分复制过来即可。

def mergeSort(nums):
    n =len(nums)
    if n<=1:
        return nums
    mid = n//2
    nums_left = mergeSort(nums[:mid])
    nunms_right = mergeSort(nums[mid:])

    result = []
    i,j = 0,0
    while i<len(nums_left) and j<len(nums_right):
        if nums_left[i]<nums_right[j]:
            result.append(nums_left[i])
            i+=1
        else:
            result.append(nums_right[j])
            j+=1
    #  如果左右数组长度不对称、直接将剩余的元素添加到result中
    result+=nums_left[i:]   
    result+=nums_right[j:]   
    return result

6.快速排序(Quick Sort)

通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

快速排序算法的原理如下:

  1. 从数列中挑出一个元素,称为"基准"(pivot),
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
def quickSort(nums,first,last):
    if first >= last:
        return
    i,j = first,last
    base = nums[first]
    #两全等状态时,快速排序起始项一定要从非基数base侧开始,不然基数无法调换
    while i < j:            #快速排序起始项一定要从非基数base侧开始,不然基数无法调换
        while i < j and nums[j] >= base: #注意一定要加'='号,不然会进左右呼唤的死循环
            j -= 1
        nums[i],nums[j] = nums[j],nums[i]  
        
        while i < j and nums[i] <= base: #注意一定要加'='号,不然会进左右呼唤的死循环
            i += 1
        nums[i],nums[j] = nums[j],nums[i]
        
    quickSort(nums,first,i-1)
    quickSort(nums,j+1,last)

练习

1. 桶排序(347)

题目描述:
给定一个数组,求前 k 个最频繁的数字。
output_1
解题思路:
桶排序的意思是为每个值设立一个桶,桶内记录这个值出现的次数(或其它属
性),然后对桶进行排序。针对样例来说,我们先通过桶排序得到三个桶 [1,2,3,4],它们的值分别
为 [4,2,1,1],表示每个数字出现的次数。

  1. 首先遍历一遍 得出数字和次数的关系(map)
  2. 然后定义一个数组(桶) 由于k不会大于数组长度。所以桶的长度len(nums)+1即可
  3. 第k(k-1, k-2, k-3 …)大的次数就是第k个桶。 值就是对应的数字。(tmp[v] = num)。考虑到可能出现相同频率的数字(例如 11122212364 1和2的频率相同),所以桶是个二维数组。用来存放频率相同的数字(tmp[v] = append(tmp[v], num))
  4. 最后倒着遍历桶,取出桶里第二维数组里的数字。取够k个就 return
  5. 这样下来相当于遍历了3遍。时间复杂度n
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
	cnt = collections.Counter(nums)
	bucket = dict()
	for x,v in cnt:
		if v not in bucket:
			bucket[v]=[x]
		else:
			bucket[v].append(x)
	ans = []
	for v in range(len(nums)-k+1,-1,-1):
		if v in bucket:
			ans.expend(bucket[v])
		if len(ans)>=k:
			return ans[:k]
2. 根据字符出现频率排序(451)

题目描述:
给定一个字符串 s ,根据字符出现的 频率 对其进行 降序排序 。一个字符出现的 频率 是它出现在字符串中的次数。

解题思路:
与桶排序思路类似

def frequencySort(self, s: str) -> str:
        cnt = collections.Counter(s)
        bucket = dict()
        ans = []

        for x,v in cnt.items():
            if v not in bucket:
                bucket[v] = [x*v]
            else:
                bucket[v].append(x*v)
        
        for v in range(len(s),0,-1):
            if v in bucket:
                ans.extend(bucket[v])
        return ''.join(ans)
3.颜色分类(75)

题目描述:
给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。

解题思路:
使用指针 p0 来交换 0,p2 来交换 2。此时,p0 的初始值为 0,而 p2 的初始值为 n−1。在遍历的过程中,我们需要找出所有的 0 交换至数组的头部,并且找出所有的 2 交换至数组的尾部。
我们从左向右遍历整个数组,设当前遍历到的位置为 i,对应的元素为 n u m s [ i ] {nums}[i] nums[i]:

  • 如果 nums[i] == 0, 将其与 n u m s [ p 0 ] {nums}[p_0] nums[p0]进行交换,并将 p 0 p_0 p0 向后移动一个位置
  • 如果 nums[i] ==2,将其与 n u m s [ p 2 ] {nums}[p_2] nums[p2]进行交换,并将 p 2 p_2 p2 向前移动一个位置

ps:
对于第二种情况,当我们将 n u m s [ i ] 与 n u m s [ p 2 ] {nums}[i] 与 {nums}[p_2] nums[i]nums[p2]进行交换之后,新的 n u m s [ i ] {nums}[i] nums[i]可能仍然是 2,也可能是 0。然而此时我们已经结束了交换,开始遍历下一个元素 n u m s [ i + 1 ] {nums}[i+1] nums[i+1],不会再考虑 n u m s [ i ] {nums}[i] nums[i] 了,这样我们就会得到错误的答案。

因此,当我们找到 2 时,我们需要不断地将其与 n u m s [ p 2 ] {nums}[p_2] nums[p2]进行交换,直到新的 n u m s [ i ] {nums}[i] nums[i]不为 2。此时,如果 n u m s [ i ] {nums}[i] nums[i] 为 0,那么对应着第一种情况;如果 n u m s [ i ] {nums}[i] nums[i] 为 1,那么就不需要进行任何后续的操作

def sortColors(self, nums: List[int]) -> None:
	p0,p2 = 0,len(nums)-1
	i = 0
	while i<=p2:
		while i<=p2 and nums[i] == 2:
			nums[i],nums[p2] = nums[p2],nums[i]
			p2-=1
		if nums[i]==0:
			nums[i],nums[p0] = nums[p0],nums[i]
			p0+=1
		i+=1
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值