排序算法(插入、冒泡、归并、快排、堆排)

排序算法(插入、冒泡、归并、快排、堆排)

快速排序:
思路:
使用分治递归的思想,我们首先在序列中选取一个元素作为基准数(有直接选取序列序列首元素、选取序列尾元素、随机选取元素、选取序列中间元素
这几种方式。 但为了使算法表达比较统一,一般选取了基准数之后,都会把这个基准数与序列末尾元素进行交换,即把基准数放到序列末尾)。然后在序列左、
右两端各设立一个指针,两个指针交替行进,把比基准数大的元素交换到序列后面,把比基准数小的元素交换到序列前面,直到两个指针相遇。这时判断,如果
两指针相遇元素小于或等于基准数,则让指针后面位置的元素与基准数进行交换(此时指针后面位置的元素一定 >= 基准数),如果相遇元素大于基准数,则直接
与基准数进行交换。 这样,基准数左边的元素就全部 小于等于 基准数, 基准数右边的元素就全部 大于等于 基准数。 那么基准数就把序列给分成了两个子
序列,所以我们可以分治递归地用同样方式继续去处理这两个子序列。
代码:

private static void QuickSort(int[] ar)
{
    Helper(ar, 0, ar.Length - 1);
}
private static void Helper(int[] ar, int start, int end)
{
    if (start >= end) return;
    int index = end;
    int cur = ar[index];
    int l = start;
    int r = end;
    while(l < r)
    {
        while(ar[r] >= cur && r > l) --r;
        Swap(ar, r, l);
        while (ar[l] <= cur && r > l) ++l;
        Swap(ar, r, l);
    }
    if (ar[l] > cur)
    {
        Swap(ar, l, index);
        index = l;
    }
    else
    {
        Swap(ar, l + 1, index);
        index = l + 1;
    }
    Helper(ar, start, index - 1);
    Helper(ar, index + 1, end);
}
private static void Swap(int[] ar, int i, int j)
{
    int temp = ar[j];
    ar[j] = ar[i];
    ar[i] = temp;
}

最优时间复杂度:O(nlogn)
最坏时间复杂度:O(n^2)
平均时间复杂度:O(nlogn)
空间复杂度:因为使用的是递归方式,所以空间复杂度与时间复杂度相同

插入排序:
思路:
我们把序列分为两个部分:以排序序列 和 未排序序列。
一开始,我们把序列第一个元素作为以排序序列,剩下的就是未排序序列,那么我们开始遍历未排序序列,并将遍历到的元素与已排序序列的末尾元素进行比较
如果当前遍历元素比已排序序列末尾元素要小,则将其与末尾元素进行交换,并且继续向前与已排序序列中元素进行比较,如果还大则继续交换… 直到比要比
较的元素大,那么此时该元素就相当于被插入到了在已排序序列中的正确位置,已排序序列的长度也增长了1。如果当前遍历元素大于等于已排序序列末尾元素
则说明当前遍历元素已经在合适的位置了,所以已排序序列长度直接+1,继续向后遍历未排序序列即可,这样当遍历完未排序序列时,整个序列就排序完毕了。
代码:

private static void InsertSort(int[] ar)
{
    if (ar.Length <= 1) return;
    for(int i = 1; i < ar.Length; i++)
    {
        int cur = ar[i];
        for(int j = i - 1; j >= 0; j--)
        {
            if (cur < ar[j])
            {
                int temp = ar[j];
                ar[j] = cur;
                ar[j + 1] = temp;
            }
            else break;
        }
    }
}

最优时间复杂度:O(n)
最坏时间复杂度:O(n^2)
平均时间复杂度:O(n^2)
空间复杂度:O(1)

冒泡排序:
思路:
我们从序列第一个元素出发开始遍历,在序列中进行相邻元素的比较,如果遍历元素比起其相邻元素大,则交换这两个元素。这样,当我们遍历到最后一个元素
时,我们就会把一个最大元素“冒泡”到序列的最后面。这样我们的未排序序列长度就要-1。
这样,当我们对上面的操作进行 n 次(nums.Length)之后,所有的元素就都被冒泡到正确的位置了。
代码:

private static void BubbleSort(int[] ar)
{
    for(int i = 0; i < ar.Length; ++i)
    {
        for(int j = 0; j < ar.Length - 1 - i; ++j)
        {
            int cur = ar[j];
            int next = ar[j + 1];
            if(cur > next)
            {
                ar[j + 1] = cur;
                ar[j] = next;
            }
        }
    }
}

时间复杂度:O(n^2) (最好、最坏、平均)

空间复杂度:O(1)

归并排序:
思路:
分治递归的思路,把序列分为两个部分,然后继续把两个序列分为四个序列,继续分… 直到最后,我们会用一个个长度 <= 1 的序列来把整个序列
进行存储,之后我们再向上返回。
在向上返回的过程中,我们把 分出来的 左、右两个序列的元素,一一进行比较,并放入到一个新的序列中,两个序列比较完毕后,新的序列就是这
两个序列排序后的合并序列。我们把新的序列向上返回,这样新的序列就会与其它二分出来的序列进行继续进行比较。
直到向上返回完毕,返回出来的新序列就是原本序列排序后的序列。
代码:

private static int[] Sort(int[] nums)
{
    return Helper(new List<int>(nums)).ToArray();
}
private static List<int> Helper(List<int> lst)
{
    if (lst.Count <= 1) return lst;


    int mid = lst.Count / 2;
    List<int> llst = new List<int>();
    List<int> rlst = new List<int>();
    for (int i = 0; i < mid; ++i) llst.Add(lst[i]);
    for (int i = mid; i < lst.Count; ++i) rlst.Add(lst[i]);
    llst = Helper(llst);
    rlst = Helper(rlst);
    return Merge(llst, rlst);
}
private static List<int> Merge(List<int> llst, List<int> rlst)
{
    int l = 0;
    int r = 0;
    List<int> res = new List<int>();
    while(l < llst.Count && r < rlst.Count)
    {
        if(llst[l] < rlst[r]) res.Add(llst[l++]);
        else res.Add(rlst[r++]);
    }
    if(l < llst.Count)
    {
        for(; l < llst.Count; ++l)
        {
            res.Add(llst[l]);
        }
    }
    if(r < rlst.Count)
    {
        for (; r < rlst.Count; ++r)
        {
            res.Add(rlst[r]);
        }
    }
    return res;
}

最优时间复杂度:O(nlogn)
最坏时间复杂度:O(nlogn)
平均时间复杂度:O(nlogn)
空间复杂度:O(n)

堆排序:
思路:
首先我们要了解堆结构:堆就是一种二叉树结构
大顶堆:
二叉树中每个节点都大于其左右子节点,这样最大的元素就在堆顶
小顶堆:
二叉树中每个节点都小于其左右子节点,这样最小的元素就在堆顶
尽管我们说堆是一种二叉树结构,但这并不意味着我们在堆排序时需要额外的存储空间来构造二叉树。我们可以使用二叉树的序列定义,即在一个序列中,对于
下标为 i 的元素,其左节点为 2 * i + 1 下标的节点,其右节点为 2 * i + 2 下标的节点。这样树的根节点就是序列的首元素, 树的最右子节点是序列尾
这样我们就可以在待排序序列本身上进行堆的构建。
堆排序的基本步骤为:构建堆 -> 将堆顶元素与堆尾元素交换,堆长度 -1 -> 重构堆 -> 重复前面两步,直到堆长度为 0,此时整个序列排序完毕
我们来具体分析一下这些步骤:
1、构建堆。
以大顶堆为例,当我们把序列构建成为大顶堆后,我们无法保证整个序列都是有序的,但是可以保证序列的首元素,即堆的根节点,是所有元素中最大的。
即,我们构造出大顶堆后,序列中最大元素就被找到了。
那么我们再来看看,该怎样构造堆结构,同样以大顶堆为例:
这里我没有给出图片,我们可以自行地画图来看一看,我们采用的是从下而上的同时又从上而下的方式,即:
我们先找到 i = len / 2 下标下的元素,该元素就是堆的最右节点位置,
那么如果 2 * i + 1 < len && 2 * i + 2 < len 则说明该节点是最右非叶子节点,且拥有左、右子节点,那么我们就比较该节点 和 其左、右子节点,将
三者最大的元素放在该位置,即该元素与三者中最大的元素进行交换。
如果 2 * i + 1 < len && !(2 * i + 2 < len) 则说明该节点是最右非叶子节点,且拥有左子节点,没有右子节点,那么就比较该阶段 和 其左子节点,
将其与两者之间最大的进行交换
如果 !(2 * i < len) && !(2 * i + 2 < len) 则说明该节点是最右叶子节点,此时其没有要比较的对象,因此什么都不用做
这样,我们就可以保证,当前这个最小的树结构,其根节点是大于其左、右子节点的。
这只是构建堆的第一步,而在构建的过程中,我们当前遍历到的节点是有左、右子树的,由于我们进行了节点的交换,即当前最大元素被放到了正确的位置,但
被交换的元素不一定在其正确位置,我们需要把它 “沉” 到正确位置,方法很简单,我们让其 与 其左、右 节点进行比较,如果其没有其左、右子节点大,那
么我们就让其与最大的子节点进行交换,如果产生了交换,我们就继续让交换过后该节点与其子节点进行比较… 直到没有产生交换,那么该元素就被 “沉”
到了正确位置。
如果画一下图来模拟这个过程的话,会很容易理解

2、将堆顶元素与堆尾元素交换,堆长度 -1
以大顶堆为例,当我们构建出大顶堆结构后,就找到了序列中最大的元素,那么我们把该元素与序列尾元素进行交换,即把最大元素放在序列尾部,作为已排序
序列。即已排序序列长度 +1,那么我们就该让待排序序列长度 -1。

3、重构堆
之后我们再在待排序序列中重新构建大顶堆,那么就又会找出当前待排序序列中最大的元素。
这里重构堆的步骤因为我们交换过来的元素直接是在堆顶位置,所有我们采用从上向下的方式即可,即把交换过来的堆顶元素,再沉到下面即可。

4、重复前面两步
重构之后,我们就又找到一个当前待排序序列的最大元素,将其与带排序序列末尾元素进行交换,即 待排序序列长度 -1,已排序序列长度 +1
这样,直到待排序序列长度为0时,就说明整个序列已排列完毕。
其实前面的步骤,每进行一次,我们的已排序序列就会 +1, 那么当我们进行了 len 次之后,整个序列也就排序完毕了。

最后是关于排序算法的一个小 Tips:
我们在实现排序算法时,很容易觉得正序排列 和 逆序排列 即 从小到大的排列 和 从大到小的排列 是两种实现。
实际上,我们可以它们合并为一种实现,这也是 C# 中 IComparable 接口的存在意义
在下面的 堆排序 Lua 语言实现中,就展示了 ICompareable 的意义:
在排序方法中,需要传入一个 compar 参数,该参数就是一个回调函数,是用户定义的比较方法。例如我们希望是从小到大排序,那么 compar 的方法体就是
return a < b
那么在堆排序时,我们调用 compar(a, b) 如果返回为 true,则是说明 a 比 b 小,那么我们就把 a 往下 “沉”,把 b 往上 “冒”,这样最终就会构造出
大顶堆结构,那么最终就会完成从小到大的排列
如果 compar 的方法体是 return a > b 那么我们的程序就会构建出 小顶堆,进行从 大 到 小 的排序。

代码:
Lua 因为写这一部分之前是在练 Lua,在 Lua 里写了些数据结构,写了排序算法,然后发现本文竟然没有写 堆排序?! 所以就正好给出我在 Lua 中的实现

local function Heaf(ar, index, len, compar)
    local l = index * 2 + 1
    local r = index * 2 + 2
    local largest = index


    if l <= len and (not compar(l, largest)) then
        largest = l
    end
    if r <= len and (not compar(r, largest)) then
        largest = r
    end


    if largest ~= index then
        ar[largest], ar[index] = ar[index], ar[largest]
        Heaf(ar, largest, len)
    end
end
local function BuildHeaf(ar, len, compar)
    for i = len // 2, 1, -1 do
        Heaf(ar, i, len, compar)
    end
end
local function HeafSort(ar, len, compar)
    -- 建堆
    BuildHeaf(ar, len, compar)
    -- 将堆底元素与堆顶元素交换, 并重构堆
    for i = len, 1, -1 do
        ar[1], ar[len] = ar[len], ar[1]
        len = len - 1
        Heaf(ar, 0, len, compar)
    end
end

C#:

static void Main(string[] args)
{
    Random random = new Random();
    int[] ar = new int[10];
    for(int i = 0; i < 10; i++)
    {
        ar[i] = random.Next(0, 10);
        Console.WriteLine(ar[i]);
    }
    Console.WriteLine("------------------------------------");
    Sort(ar);
    for (int i = 0; i < 10; i++)
    {
        Console.WriteLine(ar[i]);
    }
}


private static void Sort<T>(T[] ar) where T : IComparable
{
    // 构建堆
    BuildHeaf(ar);
    // 将堆顶 和 堆尾元素进行交换,待排序序列 -1(这里用 i 来标记)
    // 然后重构堆
    // 重复 len 次
    for (int i = ar.Length - 1; i >= 0; --i)
    {
        T temp = ar[i];
        ar[i] = ar[0];
        ar[0] = temp;
        Heaf(ar, 0, i);
    }
}
private static void Heaf<T>(T[] ar, int index, int len) where T : IComparable
{
    int l = 2 * index + 1;
    int r = 2 * index + 2;
    int largest = index;


    if(l < len && ar[l].CompareTo(ar[largest]) >= 0)
    {
        largest = l;
    }
    if(r < len && ar[r].CompareTo(ar[largest]) >= 0)
    {
        largest = r;
    }


    // 发生了交换
    if(largest != index)
    {
        // 进行交换
        T temp = ar[index];
        ar[index] = ar[largest];
        ar[largest] = temp;
        // 从上向下递归,维护堆结构
        Heaf(ar, largest, len);
    }
}
private static void BuildHeaf<T>(T[] ar) where T : IComparable
{
    for(int i = ar.Length / 2; i >= 0; --i)
    {
        Heaf(ar, i, ar.Length);
    }
}

时间复杂度:O(nlogn) (最好、最坏、平均)
空间复杂度:这里使用的是 递归实现,所以为 O(nlogn) 也有说是 O(n),这里 O(nlogn) 是我个人理解

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值