本文对于排序算法的原理不会做过多的介绍,仅用于总结同时方便记忆每种算法的原理
解释一些术语
这里所谓的外部排序并不是说一定只能适用于不能在内存中完成排序的状况,才被称作外部排序。而是因为根据其算法的特性,可以用作处理需要外部排序的状况。而且有些情况下,外部排序的算法效率更加优于内部排序算法。
另外,如果没有特别指出,以下默认实现的为升序排序(实现出来与降序的区别也没差到哪去)
冒泡排序(Bubble Sort),通过比较交换的方式,每一轮冒一个最小(最大)到顶端。
C++ 经典写法
template<typename T>
void BubbleSort(T arr[], int len) {
for (int i = 0; i < len; i++) {
for (int j = 1; j < len - i; j++) { //每轮冒一个放到最后
if (arr[j - 1] > arr[j]) {
swap(arr[j - 1], arr[j]);
}
}
}
}
这种经典的写法,可以进行一个小小的改进:当某轮未发生任何交换时,即可中止算法
template<typename T>
void BubbleSort(T arr[], int len) {
for (int i = 0; i < len; i++) {
bool flag = false;
for (int j = 1; j < len - i; j++) { //每轮冒一个放到最后
if (arr[j - 1] > arr[j]) {
swap(arr[j - 1], arr[j]);
flag = true;
}
}
if (!flag) break;
}
}
选择排序(Selection Sort),这里指直接选择排序,每一轮找到一个最小(最大)往前放。与冒泡的区别就是,冒泡排序是通过不断地比较交换完成“冒泡”这个操作;而选择排序则是一轮比较完后再进行交换。
C++ 经典写法
template<typename T>
void SelectionSort(T arr[], int len) {
for (int i = 0; i < len - 1; i++) {
int min = i;
for (int j = i + 1; j < len; j++) {
if (arr[j] < arr[min]) min = j;
}
swap(arr[i], arr[min]);
}
}
插入排序(Insertion Sort),插入排序就和打扑克理牌一样,这里我们通常指的是直接插入排序。
C++ 经典写法
template<typename T>
void InsertionSort(T arr[], int len) {
for (int i = 1; i < len; i++) {
T tmp = arr[i];
int j = i - 1;
while ((j >= 0) && (tmp < arr[j])) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = tmp;
}
}
希尔排序(Shell’s Sort)是插入排序的一种又称缩小增量排序(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。
C++ 经典写法
gap
template<typename T>
void ShellInsertionSort(T arr[], int len, int start, int gap) {
for (int i = start + gap; i < len; i += gap) {
T tmp = arr[i];
int j = i - gap;
while ((j >= 0) && (tmp < arr[j])) {
arr[j + gap] = arr[j];
j -= gap;
}
arr[j + gap] = tmp;
}
}
template<typename T>
void ShellSort(T arr[], int len) {
for (int gap = len / 2; gap > 0; gap /= 2) {
for (int i = 0; i < gap; i++) {
ShellInsertionSort(arr, len, i, gap);
}
}
}
归并排序(Merge Sort)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。另,归并排序也是一种外部排序。
注:通过查询资料我们可以知道,有一种改进的归并排序可以使最好情况的时间复杂度降至 O(n)
,甚至可以完成原地归并即空间复杂度为 O(1)
。但我们这里仅讨论经典的方法。
C++ 经典写法
mid
时,直接计算一般为 int mid = (left + right) / 2
但这不是一个好方法,此法容易导致溢出。有一个不会导致溢出的方法,养成好习惯 int mid = (right - left) / 2 + left
template<typename T>
void Merge(T arr[], int left, int mid, int right) {
int i = 0, p1 = left, p2 = mid + 1, tmpLen = right - left + 1;
T *tmp = new T[tmpLen]; //额外空间最大来源
while (p1 <= mid && p2 <= right) {
if (arr[p1] <= arr[p2]) { //这里比较要写 <= 否则会不稳定
tmp[i] = arr[p1];
p1++;
}
else {
tmp[i] = arr[p2];
p2++;
}
i++;
}
while (p1 <= mid) {
tmp[i] = arr[p1];
i++; p1++;
}
while (p2 <= right) {
tmp[i] = arr[p2];
i++; p2++;
}
for (i = 0; i < tmpLen; i++) {
arr[left + i] = tmp[i];
}
delete[]tmp; //这种方法不要忘记释放数组
}
template<typename T>
void MergeSort(T arr[], int left, int right) {
if (left >= right) return;
int mid = (right - left) / 2 + left; //此写法防止 int 溢出
MergeSort(arr, left, mid);
MergeSort(arr, mid + 1, right);
Merge(arr, left, mid, right);
}
快速排序(Quick Sort)是对冒泡排序的一种改进。每一轮都能确定 pivot
的正确位置。
算法流程如下:
Find Pivot
首先设定一个分界值,通过该分界值将数组分成左右两部分;Partition
根据分界值,小于分界值的放一边,大于分界值的放一边Recursion
左边递归,右边递归C++ 经典写法
template<typename T>
int PartitionStandard(T arr[], int left, int right) {
T pivot = arr[left];
while (left < right) {
while (left < right && arr[right] >= pivot) {
right--;
}
arr[left] = arr[right];
while (left < right && arr[left] <= pivot) {
left++;
}
arr[right] = arr[left];
}
arr[left] = pivot;
return left;
}
template<typename T>
void QuickSort(T arr[], int left, int right) {
if (left >= right) return;
int pivot = PartitionStandard(arr, left, right);
QuickSort(arr, left, pivot - 1);
QuickSort(arr, pivot + 1, right);
}
堆排序(Heap Sort)是指利用堆这种数据结构所设计的一种排序算法。
我们这里以使用大根堆为例,算法包括以下三个重点:
在这里堆(二叉树的结构)是用数组模拟的,我在这里提示几个写代码的重点
len/2 - 1
,len 为数组长度parent*2 + 1
,parent 为当前节点的位置C++ 经典写法
template<typename T>
void MaxHeapify(T arr[], int start, int end) {
int parent = start, child = parent * 2 + 1;
while (child <= end) {
if (child + 1 <= end && arr[child] < arr[child + 1]) { //比较两个子节点大小,选择大的
child++;
}
if (arr[parent] > arr[child]) return; //父节点大于两个子节点,调整完毕
else { //否则,父子节点交换,然后沿着子节点向下继续调整
swap(arr[parent], arr[child]);
parent = child;
child = parent * 2 + 1;
}
}
}
template<typename T>
void HeapSort(T arr[], int len) {
for (int i = len / 2 - 1; i >= 0; i--) { //BuildMaxHeap,从最后一个父节点开始,然后向上调整
MaxHeapify(arr, i, len - 1);
}
for (int i = len - 1; i > 0; i--) { //HeapSort core code
swap(arr[0], arr[i]); //把最大的放在数组尾部
MaxHeapify(arr, 0, i - 1); //向下调整
}
}
若没有明确指出,接下来, k k k 指代桶的数量
计数排序(Counting Sort)是一个非基于比较的排序算法,这种排序其实就是利用空间换时间,可以理解为使用额外的 hash
数组完成排序。一般状况下,它的效率优于所有基于比较的排序。但需要注意的是,当
O
(
k
)
>
O
(
n
l
o
g
n
)
O(k)>O(nlogn)
O(k)>O(nlogn) 的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn), 如归并排序,堆排序)
C++ 经典写法
void CountingSort(int arr[], int len) {
int max = arr[0];
for (int i = 1; i < len; i++) { //找到最大值
if (arr[i] > max) max = arr[i];
}
int *hash = new int[max + 1];
memset(hash, 0, sizeof(int) * (max + 1));
for (int i = 0; i < len; i++) { //对应计数
hash[arr[i]]++;
}
for (int i = 0, j = 0; j < max + 1; j++) {
while (hash[j] > 0) { //写回数组
arr[i] = j;
hash[j]--;
i++;
}
}
delete[]hash;
}
桶排序(Bucket Sort)也称箱排序,可以认为是计数排序的升级版。计数排序一个坑只能让同值的站,而桶排序一个桶里面放的是一个范围内的值,桶内再排序。
了解了原理之后,我们就会明白
这是我的一种写法,也算不上纯 C++ 风格,仅供参考,有几点需要注意的
bucketNum
是自行设定的,根据这个数量自动计算出每个桶容纳的值范围template<typename T>
struct ListNode {
T key;
ListNode* next;
ListNode(T v = 0) :key(v), next(nullptr) {}
};
template<typename T>
void InsertInOrder(ListNode<T>*& head, T val) {
ListNode<T>* dummy = new ListNode<T>(), * newNode = new ListNode<T>(val);
ListNode<T>* pre, * curr;
dummy->next = head;
pre = dummy;
curr = head;
while (nullptr != curr && curr->key <= val) { //按序插入
pre = curr;
curr = curr->next;
}
newNode->next = curr;
pre->next = newNode;
head = dummy->next;
delete dummy; //释放内存
}
template<typename T>
int CalculateBucketRange(T arr[], int len, int bucketNum) {
T max = arr[0];
for (int i = 1; i < len; i++) { //找最大值
if (arr[i] > max) max = arr[i];
}
return max / (bucketNum-1); //这里要除 bucketNum-1
}
template<typename T>
void BucketSort(T arr[], int len, int bucketNum) {
ListNode<T>** bucket = new ListNode<T> * [bucketNum]; //指针数组
int bucketRange = CalculateBucketRange(arr, len, bucketNum);
for (int i = 0; i < bucketNum; i++) { //初始化指针数组
bucket[i] = nullptr;
}
for (int i = 0; i < len; i++) { //遍历数组,把数组放桶里
InsertInOrder(bucket[arr[i] / bucketRange], arr[i]);
}
for (int i = 0, j = 0; i < bucketNum; i++) { //遍历每个桶,把值摘下来
if (bucket[i] == nullptr) continue;
ListNode<T>* p = bucket[i], * tmp = nullptr;
while (p != nullptr) {
arr[j] = p->key;
tmp = p;
p = p->next;
delete tmp; //赋值之后清除内存
j++;
}
bucket[i] = nullptr;
}
delete[]bucket; //释放指针数组
}
int main() {
int arr[] = { 45,12,34,77,90,11,2,4,5,55,6,6,6,7,7,7,20,20,99,99,199};
int len = sizeof(arr) / sizeof(int);
int bucketNum = 10;
PrintArray(arr, len);
BucketSort(arr, len, bucketNum);
PrintArray(arr, len);
}
基数排序(Radix Sort),将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列
C++ 写法
int GetMaxbit(int arr[], int len) { //数据最大位数
int maxbit = 1; //保存最大的位数
int t = 10;
for (int i = 0; i < len; i++) {
while (arr[i] >= t) {
t *= 10;
maxbit++;
}
}
return maxbit;
}
void RadixSort(int arr[], int len) //基数排序
{
int maxbit = GetMaxbit(arr, len);
int cnt[10], * tmp = new int[len]; // cnt计数,tmp暂存数值
int radix = 1;
for (int i = 0; i < maxbit; i++) { //进行maxbit次排序
memset(cnt, 0, sizeof(cnt)); //计数清 0
for (int j = 0; j < len; j++) { //统计每个桶中的记录数
int index = (arr[j] / radix) % 10;
cnt[index]++;
}
for (int j = 1; j < 10; j++) { //将tmp中的位置依次分配给每个桶
cnt[j] = cnt[j - 1] + cnt[j];
}
for (int j = len - 1; j >= 0; j--) { //将所有桶中记录依次收集到tmp中
int index = (arr[j] / radix) % 10;
tmp[cnt[index] - 1] = arr[j];
cnt[index]--;
}
for (int j = 0; j < len; j++) { //将临时数组的内容复制到data中
arr[j] = tmp[j];
}
radix *= 10;
}
delete[]tmp;
}
排序名称 | 平均时间复杂度 | 最好情况 | 最坏情况 | 额外空间复杂度 | 类型 | 稳定性 |
---|---|---|---|---|---|---|
冒泡排序 | O(n^2) | O(n) | O(n^2) | O(1) | 内部 | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 内部 | 不稳定 |
插入排序 | O(n^2) | O(n) | O(n^2) | O(1) | 内部 | 稳定 |
希尔排序 | O(n^1.5) | O(n^1.3) | O(n^2) | O(1) | 内部 | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 外部 | 稳定 |
快速排序 | O(nlogn) | O(nlogn) | O(n^2) | O(logn) | 内部 | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 内部 | 不稳定 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(k) | 外部 | 稳定 |
桶排序 | O(n+n(logn-logk)) | O(n) | O(n+k) | O(n+k) | 外部 | 稳定 |
基数排序 | O(nklogr) | - | - | O(n+k) | 外部 | 稳定 |