一、插入排序
插入排序的思路:将数据列分成两组,一组是已排序的序列A,另一组是未排序的序列B。遍历B将B中的元素插入到A中,从而使序列有序。
1.1 直接插入排序
将序列分成两部分,从头到尾遍历序列完成排序。现在我们有:
- 序列L[0, n-1]
- 有序序列A[0, i-1]
- 无序序列B[i, n-1]
算法简述:
- 选择B的头部,顺序查找找到其在A中插入的位置k
- 将B插入到L[k];向后移动L[k, i-1],到L[k+1, i]。
- 重复直到遍历完数组
算法分析:
- 空间效率:复杂度O(1)
- 时间效率:在排序过程中,向A中指向了n-1次插入操作。在最好情况下,只用插入不用移动,时间复杂度为O(n);在最坏情况下,每次插入都移动i+1次,移动的总次数为:
。注意,这里下标i从1开始,数组的1号位置下标为0。在平均情况下,时间复杂度约为n2 / 4。因此其时间复杂度为O(n2)。 - 稳定性:因为是从后向前比较(或是反之),所以不用担心相同元素的相对位置发生变化,之间插入排序是一个稳定的排序算法。
- 适用性:顺序表、链表。
1.2 折半插入排序
和直接插入排序一样,只是查找的过程变为折半查找。
1.3 希尔排序
算法描述:
- 初始化 di = n / 2;
- 选取步长di,将序列分成间隔di的n/di组;
- 在组内进行插入排序;
- 令di+1 = di / 2;
- 重复(1, 4)步直到di == 1;
算法分析:
- 空间效率:复杂度O(1)。
- 时间效率:平均情况下为O(n1.3),最坏情况下为O(n2)
- 稳定性:分组的原因可能会改变相同元素的相对位置,其是不稳定的排序方法。
- 适用性:顺序表。
二、交换排序
2.1 冒泡排序
大部分人学习的第一个排序方法,按照排序准则将“最大/小”的元素放入序列的首部或末尾。
void BubbleSort(std::vector<ElementType>& Seq)
{
// i表示第i次排序,此时针对第i个元素,比较它和序列中剩下元素的大小,选出最小的放入第i个位置(交换ij的位置)
for (int i = 0; i < Seq.size(); ++i)
{
// 用于判断本次(i)是否发生了交换
bool flag { false };
for (int j = i; j < Seq.size(); ++j)
{
// 这里使用 < 作为排序准则
if (Seq[i] < Seq[j])
{
swap(Seq[i], Seq[j]);
flag = true;
}
}
// 如果没有发生交换,则说明序列已经有序
if (!flag) return;
}
return;
}
算法分析:
- 空间效率:swap需要使用一个中间变量,复杂度O(1)。
- 时间效率:当序列有序时,比较n-1次、移动0次,从而最好情况下的时间复杂度为O(n);当序列逆序时,比较:Sigmai=1n-1(n-i) = n(n-1) / 2、移动Sigmai=1n-13(n-i) = 3n(n-1) / 2,从而最坏情况下时间复杂度为O(n2)。
- 稳定性:不改变相同元素的相对位置,是稳定的排序方法。
2.2 快速排序
下面我们通过一个实例来说明,考虑序列:49、38、65、97、76、13、27、49。这里使用“<”作为排序准则。
void QuickSort(std::vector<ElemType>& seq, int head, int tail)
{
if (head < tail) // 递归跳出条件
{
// 划分序列
int pos = Partition(seq, head, tail);
// 子序列快排
QuickSort(seq, head, pos - 1);
QuickSort(seq, pos + 1, tail);
}
}
// 序列分组
int Partition(std::vector<ElemType>& seq, int head, int tail)
{
OP op;
while (head < tail)
{
// 移动尾部指针,意图找到!(*head op *tail)
while (head < tail && op(seq[head], seq[tail])) --tail;
swap(seq[head], seq[tail]);
// 移动首部指针,意图找到!(*head op *tail)
while (head < tail && op(seq[head], seq[tail])) ++head;
swap(seq[head], seq[tail]);
}
return head;
}
// 排序准则,这里使用<=
class OP
{
public:
bool operator()(ElemType A, ElemType B)
{
return A <= B;
}
}
算法分析:
- 空间效率:递归需要借助一个工作栈完成,其容量与递归深度一致,最好情况下为O(log2n),最坏情况下为O(n-1),平均为O(log2n)。
- 时间效率:O(n2)。
- 稳定性:子序列以及排序准则(<或<=一类)的选择,其不稳定。
三、选择排序
3.1 简单选择排序
算法描述:
// 这里使用升序排序
void SelectSort(std::vector<ElemType> seq)
{
for (int i = 0; i < seq.size(); ++i)
{
// 选取最小元素的*位置*
ElemType minElePos = i;
for (int j = i + 1; j < seq.size(); ++j)
{
minElePos = (i < j) ?
i:
j;
}
if (minElePos != i) swap(seq[i], seq[minElePos]);
}
}
算法分析:
- 空间效率:swap消耗1个单位的中间变量,O(1)。
- 时间效率:O(n2)。
- 稳定性:不稳定。
3.2 堆排序
一个看着好像强无敌的排序方法,实际上没有那么强无敌,可能直接建立avl的效率更高。下面我们来看一下什么是堆、大根堆、小根堆。
在上图中,L(i) >= max(L(2i), L(2i+1)),其是一个大根堆,也可以将该数组视为一个完全二叉树。大根堆的最大元素存放在堆顶,其任意非根节点的值小于其父节点。
堆排序的简单思路:
- 构造大根堆、小根堆
- 遍历,调整最大、最小元素到堆顶
- 移除堆顶元素,将最末元素移动到堆顶
- 重复直到堆中只有一个元素
下面我们通过一个实例来演示:
/* ===== 重复构造大根堆 ===== */
void BuildMaxHeap(std::vector<ElemType>& seq, int len)
{
// 下标范围[0, len-1]
for (int i = len / 2; i >= 0; --i)
{
HeadAdjust(seq, i, len);
}
}
/* ===== 节点移动 ===== */
void HeadAdjust(std::vector<ElemType>& seq, int rootK, int len)
{
// 对以rootK为根节点的子树进行调整
ElemType elemRootK = seq[rootK];
// 下标范围[0, len-1]
for (i = 2 * rootK; i < len; i *= 2)
{
// 选取更大的子节点,i或i+1
if (i < len && seq[i] < seq[i+1]) i++;
// 已经满足大根堆,跳出
if (elemRootK > seq[i])
{
break;
}
else
{
// 将i的值交给rootK,继续查看i的子树是否需要调整
seq[rootK] = seq[i];
rootK = i;
}
}
seq[rootK] = elemRootK;
}
/* ===== 堆排序 ===== */
void HeapSort(std::vector<ElemType>& seq)
{
BuildMaxHeap(seq, seq.size());
for (i = seq.size(); i > 0; --i)
{
// 移出堆顶seq[0]到堆底
swap(seq[i], seq[0]);
// 调整剩余的元素
HeadAdjust[seq, 0, i-1];
}
}
算法分析:
- 空间效率:O(1)。
- 时间效率:O(nlog2n)。
- 稳定性:不稳定。
四、并归排序和基数排序
4.1 并归排序
将数组分为n个列表,通过不断合并列表来实现排序,下面我们通过一个列子来演示:
/*
有序合并seq[low, mid-1]和seq[mid, high-1]
*/
void Merge(std::vector<ElemType>& seq, int low, int mid, int high)
{
// 用于存放seqLow和seqHigh的合并结果,最后写回到seq中
std::vector<ElemType> temp {seq};
for (int i{ low }, j{ high }, k{ 0 }
i < mid && j < high;
++k;)
{
seq[k] = (temp[i] < temp[j]) ?
temp[i++]:
temp[j++];
}
// 若seqL(tempL)未检测完,则全部拷贝入seq
while (i < mid) seq[k++] = temp[i++];
// 若seqH(tempH)未检测完,则全部拷贝入seq
while (j < high) seq[k++] = temp[j++];
}
void MergeSort((std::vector<ElemType>& seq, int low, int high)
{
if (low < high)
{
int mid = (low + high) / 2;
// 递归排序左子列
MergeSort(seq, low, mid);
// 递归排序右子列
MergeSort(seq, mid+1, high);
//合并
Merge(seq, low, high);
}
}
算法分析:
- 空间效率:O(n)。
- 时间效率:O(nlog2n)。
- 稳定性:稳定。
4.2 基数排序
看名字说得很抽象,下面我们来看一个实例:
算法分析:
- 空间效率:O(r),r个队列的头、尾指针。
- 时间效率:O(d(n+r)),即d次分配+收集,每次分配构造使用n的时间,收回使用r的时间。
- 稳定性:稳定。
Comments | NOTHING