[算法整理]排序算法

排序算法汇总

总结了各种排序算法,代码由python实现。

冒泡排序

依次比较两个邻居,如果前者比后者大,则交换顺序,如此遍历k次,则最后k个元素处于正确的顺序,因此,遍历n次即可解决问题;

def bubble(ll: List) -> List:
    n = len(ll)
    for i in range(n):
        for j in range(n - i - 1):
            if ll[j] > ll[j + 1]:
                tem = ll[j]
                ll[j] = ll[j + 1]
                ll[j + 1] = tem
    return ll

遍历n次,第i次遍历的时候,进行了ni1次对比,时间复杂度是o(n2),没有额外的使用空间,因此空间复杂度为o(1),在判断的时候,如果等于就不更改两个数字,所以是稳定排序;

另外,冒泡排序可以用来求一个数组中的逆序对,更换的次数就是逆序对的个数;

插入排序

遍历数据,每次将数据插入到前面有序数组中正确的位置;

def insert(ll: List) -> List:
    for i in range(1, len(ll)):
        v = ll[i]
        j = i - 1
        while j >= 0:
            if ll[j] > v:
                ll[j + 1] = ll[j]
                j -= 1
            else:
                break
        ll[j + 1] = v
    return ll

n个数据,每次插入,移动数据的平均时间复杂度是o(n),所以,总的时间复杂度是o(n2),空间复杂度是o(1),每次插入,后插进来的元素放在所有相等元素的后面,便可保证排序的稳定;

选择排序

遍历数组,每次选择一个最小的和当前数字进行交换;

def select(ll: List) -> List:
    n = len(ll)
    for i in range(n):
        m = i
        for j in range(i, n):
            if ll[j] < ll[m]:
                m = j
        tem = ll[i]
        ll[i] = ll[m]
        ll[m] = tem
    return ll

n个数据,每次寻找最小值平均时间复杂度o(n),总复杂度为o(n2),空间复杂度o(1),不稳定,会更改同元素的顺序;

快速排序

主要思路是选择一个元素r,然后将该数组分为三个部分:小于r的元素,r,大于r的元素;然后对小于r的元素和大于r的元素进行排序,元素r则处于正确的位置;

主要的难点在于区间分组,算法操作如下:

  • 选择随机一个元素作为锚点,然后将该元素放到最后一个位置(下面的代码直接采用最后一个元素作为锚点)
  • 设置两个指针,其中一个指针i指向的位置为锚点应该处于的位置,另一个指针j遍历除了锚点以外的所有元素:
    • 如果该元素小于锚点,则应该和i所指元素交换位置,同时令i指向下一个位置;
  • 最终交换锚点和i所指位置的元素即可;
# 区间都是前开后闭
def quick_sort(ll: List) -> List:
    def quick(nums: List, p, q):
        if q - p <= 1:
            return
        r = quick_partition(nums, p, q)
        quick(nums, p, r)
        quick(nums, r+1, q)

    quick(ll, 0, len(ll))
    return ll


def quick_partition(ll: List, p, q):
    v = ll[q - 1]
    # i,j两个指针,i所指位置之前,为小于锚点的元素
    i, j = p, p
    while j < q - 1:
        if ll[j] < v:
            tem = ll[i]
            ll[i] = ll[j]
            ll[j] = tem
            i += 1
        j += 1
    tem = ll[i]
    ll[i] = v
    ll[q - 1] = tem
    return i

时间复杂度,极端的情况下,如果每次选择的锚点都恰好在端点处,那么每次归位的就一个锚点,所以,最终的时间复杂度为o(n2),除了极端情况,其他的时候,复杂度为o(nlogn),空间复杂度为o(1),另外,不是稳定排序;

寻找第K大元素

def find_kth_largest(nums: List[int], k: int) -> int:
    t = len(nums) - k
    return nums[self.find(nums, 0, len(nums), t)]

def find(nums: List[int], p, q, t):
    while q-p>1:
        i = self.partition(nums, p, q)
        if i==t:
            return i
        if i<t:
            p=i+1
        else:
            q=i
    return p

def partition(nums: List[int], p, q):
    v = nums[q - 1]
    i, j = p, p
    while j < q - 1:
        if nums[j] < v:
            self.swap(nums, j, i)
            i += 1  
        j += 1
    self.swap(nums, i, q - 1)
    return i

def swap(nums, i, j):
    tem = nums[i]
    nums[i] = nums[j]
    nums[j] = tem

利用快速排序,每次返回的锚点如果恰好是第K大元素,则直接返回,如果不是,则缩小区间后搜索;

归并排序

主要的思想为分而治之,每次将整个区间均分为两份,然后在分别对两个区间调用排序算法,然后再对两个有序区间进行合并,递归的平凡情况,当区间内仅有一个元素的时候,直接返回;

def merge_sort(ll: List) -> List:
    def _merge_sort(nums, p, q):
        if q - p <= 1:
            return
        r = p + ((q - p) >> 1)
        _merge_sort(nums, p, r)
        _merge_sort(nums, r, q)
        res = merge(nums, p, r, r, q)
        for index, num in enumerate(res):
            nums[index + p] = num

    _merge_sort(ll, 0, len(ll))
    return ll


def merge(ll: List, p, q, m, n):
    temp = []
    while p < q and m < n:
        if ll[p] <= ll[m]:
            temp.append(ll[p])
            p += 1
        else:
            temp.append(ll[m])
            m += 1
    while p < q:
        temp.append(ll[p])
        p += 1
    while m < n:
        temp.append(ll[m])
        m += 1
    return temp

每次递归调用都是减去了一半的元素,时间复杂度计算公式为:T(n)=2T(n/2)+n,于是时间复杂度为o(nlogn),由于合并的时候需要用到临时数组,于是空间复杂度为o(n)

多路归并中的合并算法

问题的关键是需要在多路归并中快速寻找到最小值,通常的方法,对k路归并而言,每次获取最小值的时间复杂度为o(k),也可以通过最小堆的方式获取,建堆的时间复杂度为o(k),但是每次获取最小值的复杂度为o(logn),具体的方案在堆排序里会将,还有一种方案就是败者树/胜者树,同样建树的时候复杂度为o(k),获取最小值的复杂度为o(logn)

class LoserTree:
    def __init__(self):
        """
        初始化,数据和数据索引共同构成了这个败者树
        """
        # 初始的数据数组(用来构建败者树的)
        self.data = []
        # 构建起来的败者树,用数组表示,数组内记录的是数据数组对应的索引,其中0号元素代表整个败者树的胜利者的索引
        self.inner = []

    def build(self, lists):
        """
        构建一个败者树,列表内元素需要能够比较大小
        :param lists: 要构建的列表内元素
        :return:
        """
        self.inner = [-1] * len(lists)
        self.data = lists
        self.inner[0] = self.__build(1)

    def __build(self, index):
        """
        语义:在这个索引处构建败者树,返回后续构建起来的胜利者的数据的索引
        :param index: 在某个索引处
        :return: 构建起来的树的胜利者
        """
        if index >= len(self.data):
            return index - len(self.data)
        left = index << 1
        right = index << 1 + 1
        left_win = self.__build(left)
        right_win = self.__build(right)
        win, loser = self.play_game(left_win, right_win)
        self.inner[index] = loser
        return win

    def play_game(self, l1, l2):
        """
        比较两个元素,以小的那个作为胜利者,返回这次比较的胜者和败者,对于空元素,直接视为败者
        :param l1:
        :param l2:
        :return:
        """
        if self.data[l1] is None:
            return l2, l1
        if self.data[l2] is None:
            return l1, l2
        if self.data[l1] < self.data[l2]:
            return l1, l2
        else:
            return l2, l1

    def replay(self, l1):
        """
        将整棵树的胜利者置换为对应的数据节点,从而重新构建败者树:每次仅需要和上次比赛的败者(父节点)进行比赛即可,逐次向上
        :param l1:
        :return:
        """
        win = self.inner[0]
        self.data[win] = l1
        p = (win + len(self.data)) >> 1
        while p > 0:
            winner, loser = self.play_game(self.inner[p], win)
            self.inner[p] = loser
            win = winner
            p = p >> 1
        self.inner[0] = win

    def empty(self):
        """
        判空
        :return:
        """
        return self.get_winner() is None

    def get_winner(self):
        """
        获取整颗树的胜利者
        :return:
        """
        if self.inner:
            return self.data[self.inner[0]]

首先明确一点,数据和索引数组,两个数组共同构成了一颗满二叉树,同时,索引数组中的第0个元素记录了最终的胜者索引,而第一个元素则是整个树的根,于是左右子树的索引有:

 left = index << 1
 right = index << 1 + 1

的关系(将两个数组并入为一个数组,数据数据在后,因此所有的数据数组的索引均需要加上len(data)才能对标到树中的数组索引)

构建败者树时的模拟:

对于导出最小值,再插入一个新数据的情况的模拟:

对于胜者树,同样是类似的构建方案,只是,每个节点放置的是胜者的索引,所以,并不需要额外的一个节点来存储最终的胜利者,并且,如果某个节点发生的更改,则需要从这个节点开始,依次重新比赛,向根节点更新胜者索引;

计算逆序对

归并排序过程中可以计算逆序对

class Solution:
    def __init__(self):
        self.res = 0
    def reversePairs(self, nums: List[int]) -> int:
        self.merge_sort(nums,0,len(nums))
        return self.res
    def merge_sort(self,nums:List[int],p,q)->int:
        if q-p<=1:
            return
        r=p+((q-p)>>1)
        self.merge_sort(nums,p,r)
        self.merge_sort(nums,r,q)
        res = self.merge(nums,p,r,r,q)
        for index,num in enumerate(res):
            nums[index+p]=num
    def merge(self,ll:List[int],p,q,m,n):
        temp=[]
        while p<q and m<n:
            if ll[p]<=ll[m]:
                temp.append(ll[p])
                p+=1
            else:
                temp.append(ll[m])
                # 统计逆序对的个数
                self.res+=(q-p)
                m+=1
        while p<q:
            temp.append(ll[p])
            p+=1
        while m<n:
            temp.append(ll[m])
            m+=1
        return temp

在归并排序的过程中,归并中,后面的数字小于前面的情况,那么逆序对就存在对应对,这个数量等于前面那个数组还剩余的元素个数;

堆排序

最大堆和最小堆,利用最大堆进行排序,每次将最大值放到数组最后,然后对前i1个值进行重新堆化;

建堆的时间复杂度是o(n),排序的复杂度是o(nlogn),空间复杂度是o(1),非稳定排序;

class MaxHeap:
    def __init__(self, ll: List[int]):
        self.data = ll
        self.size = len(self.data)
        self.__init_heap()

    def __init_heap(self):
        """
        初始化堆
        从第一个非叶节点开始,逐个进行堆化,从上到下,时间复杂度为o(n)
        :return:
        """
        i = (len(self.data) >> 1) - 1
        while i >= 0:
            p = i
            while True:
                max_index = p
                r = (p << 1) + 1
                l = (p << 1) + 2
                if r < self.size and self.data[r] > self.data[p]:
                    max_index = r
                if l < self.size and self.data[l] > self.data[max_index]:
                    max_index = l
                if max_index == p:
                    break
                swap(self.data, max_index, p)
                p = max_index
            i -= 1

    def get_max(self):
        if self.data:
            return self.data[0]

    def insert(self, d: int):
        """
        插入一个节点,直接插在末尾,从下到上进行优化,时间复杂度o(logn)
        """
        self.size += 1
        last=self.size-1
        if last<len(self.data):
            self.data[last]=d
        else:    
        	self.data.append(d)
        i = last
        while i > 0:
            p = (i - 1) >> 1
            if self.data[p] < self.data[i]:
                swap(self.data, i, p)
            i = p

    def pop_max(self):
        """
        删除顶端元素,读取顶端元素后,将末尾元素放到顶端,然后从上到下进行堆化
        """
        if self.size > 0:
            res = self.data[0]
            self.data[0] = self.data[self.size - 1]
            self.size -= 1
            p = 0
            while True:
                r = (p << 1) + 1
                l = (p << 1) + 2
                max_index = p
                if r < self.size and self.data[p] < self.data[r]:
                    max_index = r
                if l < self.size and self.data[max_index] < self.data[l]:
                    max_index = l
                if max_index == p:
                    break
                swap(self.data, p, max_index)
                p = max_index
            return res

    def sort(self) -> List[int]:
        """
        堆排序,原地排序,排序后整个堆无法再次使用,这里仅做演示
        :return:
        """
        while self.size > 0:
            last = self.size - 1
            self.data[last] = self.pop_max()
        return self.data

和快速排序相比并不够好,跳跃式的数据访问对CPU不够友好,同时对于同样的数据,数据交换的次数也是明显对于快速排序的;

使用堆寻找中位数

对于一组动态数据,输出它们的中位数,数据随着时间会增加;

思路:

  1. 对初始数据进行排序
  2. 数据分为两个部分,前一半构建最大堆,后一半构建最小堆,如果数据个数为奇数,另最大堆的数据多一个
  3. 获取中位数只要获取最大堆的堆顶即可
  4. 如果有数据加入,则判断数据小于最大堆的堆顶,则加入到最大堆中,否则加入最小堆中,然后调整两个堆的数据数量,令其满足各一半的条件

桶排序

核心思想就是将数据的划分到天然具备顺序的几个桶里,然后再桶内进行排序(快速排序或者归并排序),排序后再将数据依次放回原数组,要求数据范围有限,同时,如果划分的桶的数量接近数据的总数量,且能令各个桶的数量均匀,则排序的时间复杂度为o(n),一般,例如知道数值在150之间,则可以直接列出一个大小为50的数组,数值为其索引,对于相同的值采用链表的方式记录,这样空间复杂度为o(n),同时可以保证相同元素的顺序,所以可以是稳定排序。

def bucket_sort(ll: List) -> List:
    # 假设数据均在0-50之间的整数,桶表示的数值为:0-9,10-19,20-29,30-39,40-49
    bucket = [[] for _ in range(5)]
    for i in ll:
        bucket[int(i / 10)].append(i)
    # 对每个桶进行排序
    res = []
    for l in bucket:
        quick_sort(l)
        res += l
    return res

计数排序

桶排序的特殊情况,桶的个数和元素数据范围相同,然后像上述所说使用链表来存储相同的元素,另外还有一种方案,就是通过记录相同元素的数量,然后遍历,将元素放到对应的位置;

def count_sort(ll: List) -> List:
    # 假设数据范围是0-6
    count = [0] * 7
    for i in ll:
        count[i] += 1
    # 累加
    for i in range(1, 7):
        count[i] = count[i - 1] + count[i]
    res = [None] * len(ll)
    i = len(ll) - 1
    while i >= 0:
        index = count[ll[i]] - 1
        res[index] = ll[i]
        count[ll[i]] -= 1
        i -= 1
    ll.clear()
    ll.extend(res)
    return res

基数排序

主要的思路,对要排序的数据,从最低位开始排序,对每一位都进行一次排序,最终得到整个有序数组,适合那些可以分开位数的数据,例如数字,字符串(字典序)等,对于数据位数不一致的情景,可以在高位补0;

单个位的排序需要是稳定排序,这里采用了计数排序,时间和空间复杂度均为o(n);

def count_sort_radix(ll: List, data_get) -> List:
    # 假设数据范围是0-9
    count = [0] * 10
    for i in ll:
        count[data_get(i)] += 1
    # 累加
    for i in range(1, 10):
        count[i] = count[i - 1] + count[i]
    res = [None] * len(ll)
    i = len(ll) - 1
    while i >= 0:
        index = count[data_get(ll[i])] - 1
        res[index] = ll[i]
        count[data_get(ll[i])] -= 1
        i -= 1
    ll.clear()
    ll.extend(res)
    return res


def radix_sort(ll: List) -> List:
    # 基数排序,从最低位开始进行排序,每次使用计数排序
    # 假设输入的是列表的列表,即元素已经被拆成了单独位数的列表了,且各个元素的位数相同
    if ll:
        n = len(ll[0])
        while n > 0:
            n -= 1
            count_sort_radix(ll, lambda i: i[n])
    return ll
if __name__ == '__main__':
    print(radix_sort([[1,1,0],[2,5,1],[2,3,0],[0,1,4],[0,3,4],[5,5,1]]))

希尔排序

思路:选择一个增量序列,然后将依次将这个数组视作等价的对应列数的二位数组,例如选择增量序列:1,2,3,5,8,13,将数组视作等价的13列的二维数组,对每一列进行排序(在同一列的元素进行部分排序),然后再讲数组试做8列的二维数组,再对每一列进行部分排序,...直到最终试做1列的二维数组,进行了全排序;

每次排序后都会增加序列的有序度,而如果采用输入敏感的插入排序算法进行部分排序操作,在最终的排序中,由于已经达到了一定的有序度,所以时间复杂度会降低;

采用不同的序列,时间复杂度也不一样,若采用的序列是Papernov-Stasevic序列:{1,3,7,...,2k1,...},采用插入排序作为迭代的排序算法,可以证明,整个希尔排序的时间复杂度优化为o(n3/2)

def insert_shell(ll: List, indexes: List[int]) -> List:
    """
    对选定的几个元素进行排序,indexes是一个升序的索引序列,指定了ll中的要排序的索引
    :param ll:
    :param indexes:
    :return:
    """
    for i in range(1, len(indexes)):
        # 有序的索引
        j = i - 1
        v = ll[indexes[i]]
        while j >= 0:
            if ll[indexes[j]] > v:
                ll[indexes[j + 1]] = ll[indexes[j]]
                j -= 1
            else:
                break
        ll[indexes[j + 1]] = v
    return ll


def shell_sort(ll: List):
    # 递增序列采用1,2,3,5,8,13,。。。
    n = len(ll)
    if n < 3:
        return insert_shell(ll, [i for i in range(n)])
    h = [1, 2]
    t = 3
    while n > t:
        h.append(t)
        t = h[-1] + h[-2]
    i = len(h)
    while i > 0:
        i -= 1
        w = h[i]
        # 将数组试做宽度为w的二维数组,然后对每一列进行排序
        for j in range(w):
            # 获得每一列的索引
            indexes = []
            index = j
            while index < n:
                indexes.append(index)
                index += w
            # 对第j列进行插入排序
            insert_shell(ll, indexes)
    return ll

posted @   随风EK  阅读(47)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 周边上新:园子的第一款马克杯温暖上架
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
· 使用C#创建一个MCP客户端
点击右上角即可分享
微信分享提示