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