-
排序的概念:所谓排序,就是使一串数据,按照其中的某个或某些关键字的大小,递增或递减的排序的操作。分为内部排序和外部排序两种。
-
内部排序:数据元素全部都放在内存中的排序。
-
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序(在磁盘中)。
-
常见的排序算法有八种:直接插入排序、希尔排序、直接选择排序、堆排序、冒泡排序、快速排序、归并排序和计数排序。
-
以下以“7,4,5,9,8,2,1”为例,按照丛小到大升序排序。
1.直接插入排序
-
基本思想:将需要排序的数据按其关键码值的大小逐个插入到一个已经排序好的有序序列中,直到所有数据插入完为止,得到一个新的有序序列。
-
当插入第 i (i >= 1)个元素时,前面那的arr[0], arr[1], …, arr[i - 1]已经排好序了,此时将arr[i]与arr[i - 1], arr[i - 2], arr[i - 3], …进行比较,插入到适当的位置。
-
实现代码
// 直接插入代码
#include <iostream>
#include <vector>
using namespace std;
void InsertSort(vector<int>& arr, int n)
{
//总共要进行n趟,因为共n个数
for (int i = 0; i < n - 1; ++i)
{
//单趟
// 将arr[end + 1]插入到arr[0] - arr[end]中
int end = i;
int tmp = arr[end + 1];
while(end >= 0)
{
if(tmp < arr[end])
{
arr[end + 1] = arr[end];
--end;
}
else
{
break;
}
}
arr[end + 1] = tmp;
}
}
int main()
{
vector<int> vc = { 7, 4, 5, 9, 8, 2, 1 };
InsertSort(vc, vc.size());
for (auto e : vc)
{
cout << e << " ";
}
cout << endl;
return 0;
}
- 直接插入排序的特性:
(1.)元素集合越接近有序,直接插入排序算法的时间效率越高。
(2.)时间复杂度是O(N ^ 2)
(3.)空间复杂度是O(1),它是一种稳定的排序算法。
(4.)该排序是稳定的。
2.希尔排序
-
基本思想:先选定一个整数,把待排序文件中所有记录分成个组,所有距离相同的数据分在同一组内,并对每一组内的数据进行排序。然后,取,重复上述分组和排序的工作。
-
实现代码
// 希尔排序实现代码
#include <iostream>
#include <vector>
using namespace std;
void ShellSort(vector<int>& arr, int n)
{
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;
for (int i = 0; i < n - gap; ++i)
{
// 单趟
int end = i;
int tmp = arr[end + gap];
while (end >= 0)
{
if (tmp < arr[end])
{
arr[end + gap] = arr[end];
end -= gap;
}
else
{
break;
}
}
arr[end + gap] = tmp;
}
}
}
int main()
{
vector<int> vc = { 7, 4, 5, 9, 8, 2, 1 };
ShellSort(vc, vc.size());
for (auto e : vc)
{
cout << e << " ";
}
cout << endl;
return 0;
}
- 希尔排序的特性:
(1.)希尔排序是对直接插入排序的优化。
(2.)当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序了,这样就会很快。这样整体而言,可以达到优化的效果。
(3.)希尔排序的时间复杂度是O(N ^ 1.3 - N ^ 2)
(4.)希尔排序是不稳定的。
3.直接选择排序
- 基本思想:每一次在待排序的数据元素中选出最小的一个元素(或者每一次在待排序的数据元素中选出最大的一个元素),存放在序列中的起始位置,知道全部待排序的数据元素排序完。
- 实现代码
// 直接选择排序实现代码
#include <iostream>
#include <vector>
using namespace std;
void Swap(int* p, int* q)
{
int tmp = *p;
*p = *q;
*q = tmp;
}
void SelectSort(vector<int>& arr, int n)
{
int begin = 0, end = n - 1;
int min_index, max_index;
while (begin <= end)
{
// 单趟排序
min_index = max_index = begin;
for (int i = begin + 1; i <= end; ++i)
{
if (arr[i] < arr[min_index])
min_index = i;
if (arr[i] >= arr[max_index])
max_index = i;
}
Swap(&arr[begin], &arr[min_index]);
if (max_index == begin)
max_index = min_index;
Swap(&arr[end], &arr[max_index]);
++begin;
--end;
}
}
int main()
{
vector<int> vc = { 7, 4, 5, 9, 8, 2, 1 };
SelectSort(vc, vc.size());
for (auto e : vc)
{
cout << e << " ";
}
cout << endl;
return 0;
}
- 直接选择排序的特性:
(1.)直接选择排序的效率不是很好,实际中很少用
(2.)时间复杂度:O(N ^ 2)
(3.)空间复杂度:O(1)
(4.)稳定性:不稳定
4.堆排序
-
基本思想:堆排序是利用堆这照片那个数据结构所设计的一种排序算法,是选择排序的一种。
-
升序建大堆,降序建小堆。
-
实现代码:
// 堆排序实现代码
#include <iostream>
#include <vector>
using namespace std;
void Swap(int* p, int* q)
{
int tmp = *p;
*p = *q;
*q = tmp;
}
void AdjustDown(vector<int>& arr, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && arr[child + 1] > arr[child])
++child;
if (arr[parent] < arr[child])
{
Swap(&arr[parent], &arr[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(vector<int>& arr, int n)
{
// 升序,建大根堆
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(arr, n, i);
}
// 排序
int end = n - 1;
while (end > 0)
{
Swap(&arr[0], &arr[end]);
AdjustDown(arr, end, 0);
--end;
}
}
int main()
{
vector<int> vc = { 7, 4, 5, 9, 8, 2, 1 };
HeapSort(vc, vc.size());
for (auto e : vc)
{
cout << e << " ";
}
cout << endl;
return 0;
}
- 堆排序的特性:
(1.)堆排序使用堆来选数,效率就会比直接选择排序高了很多。
(2.)时间复杂度:O(N * logN)
(3.)空间复杂度:O(1)
(4.)稳定性:不稳定
5.冒泡排序
- 基本思想:它重复地走访过要排序的数据元素,依次比较两个相邻的数据元素,如果顺序错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻数据元素需要交换,也就是说该数据元素列已经排序完成。
- 实现代码:
// 冒泡排序实现代码
#include <iostream>
#include <vector>
using namespace std;
void Swap(int* p, int* q)
{
int tmp = *p;
*p = *q;
*q = tmp;
}
void BubbleSort(vector<int>& arr, int n)
{
int end = n;
while (end > 0)
{
for (int i = 1; i < end; ++i)
{
if (arr[i - 1] > arr[i])
Swap(&arr[i - 1], &arr[i]);
}
--end;
}
}
int main()
{
vector<int> vc = { 7, 4, 5, 9, 8, 2, 1 };
BubbleSort(vc, vc.size());
for (auto e : vc)
{
cout << e << " ";
}
cout << endl;
return 0;
}
- 冒泡排序的特性:
(1.)时间复杂度:O(N ^ 2)
(2.)空间复杂度:O(1)
(3.)稳定性:稳定
6.快速排序
- 基本思想:任取待排序元素序列中的某元素作为基准值(关键字),按照该排序码将待排序集合分割成两个子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后左右子序列重复该过程,直到所有元素都排列在相应位置为止。
- 一般关键字取数组最后一个数或者第一个数或者中间的数,但是由于这样取的数会导致取得数过大或者过小,从而使效率变低。
- 如上面的例子“7,4,5,9,8,2,1”,要是取最后一个数会作为关键字,该关键字就会过小,导致效率过低。
- 所以取关键字我们用三数取中法。(取待排序列第一个元素,最后一个元素和中间一个元素三个元素中,处于中间大小的元素。)
- 所取的关键字与数组最后一个元素交换。
// 三数取中法
#include <iostream>
#include <vector>
using namespace std;
int GetMidIndex(vector<int>& arr, int left, int right)
{
int mid = left + (right - left) / 2;
if (arr[left] < arr[mid])
{
if (arr[mid] < arr[right])
return mid;
else if (arr[left] > arr[right])
return left;
else
return right;
}
else
{
if (arr[mid] > arr[right])
return mid;
else if (arr[left] < arr[right])
return left;
else
return right;
}
}
int main()
{
vector<int> vc = { 7, 4, 5, 9, 8, 2, 1 };
cout << GetMidIndex(vc, 0, vc.size() - 1) << endl;
cout << "基准值:" << vc[GetMidIndex(vc, 0, vc.size() - 1)] << endl;
return 0;
}
(1.)左右指针法:(左指针找比关键字大的数,右指针找,比关键字小的数)
-
若基准是最左边的元素,则判断时先从右边下手,若基准是最右边的元素,则判断时先从左边下手。
-
实现代码:
// 快速排序的左右指针法
#include <iostream>
#include <vector>
using namespace std;
void Swap(int* p, int* q)
{
int tmp = *p;
*p = *q;
*q = tmp;
}
int GetMidIndex(vector<int>& arr, int left, int right)
{
int mid = left + (right - left) / 2;
if (arr[left] < arr[mid])
{
if (arr[mid] < arr[right])
return mid;
else if (arr[left] > arr[right])
return left;
else
return right;
}
else
{
if (arr[mid] > arr[right])
return mid;
else if (arr[left] < arr[right])
return left;
else
return right;
}
}
int PartSort1(vector<int>& arr, int left, int right)
{
//加上三数取中法来优化
int mid = GetMidIndex(arr, left, right);
Swap(&arr[mid], &arr[right]);
int key = arr[right];
int key_index = right;
while (left < right)
{
while (left < right && arr[left] <= key)
++left;
while (left < right && arr[right] >= key)
--right;
if (left < right)
{
Swap(&arr[left], &arr[right]);
++left;
--right;
}
}
Swap(&arr[left], &arr[key_index]);
return left;
}
void QuickSort1(vector<int>& arr, int left, int right)
{
if (left >= right)
return;
int index = PartSort1(arr, left, right);
QuickSort1(arr, left, index - 1);
QuickSort1(arr, index + 1, right);
}
int main()
{
vector<int> vc = { 7, 4, 5, 9, 8, 2, 1 };
QuickSort1(vc, 0, vc.size() - 1);
for (auto e : vc)
{
cout << e << " ";
}
cout << endl;
return 0;
}
(2.)挖坑法:
- 将关键字所在数组的位置,当作一个坑,从左开始遍历,找到第一个值大于关键字的元素将其填到坑中,则第一个值大于关键字的元素原所在的数组位置是一个新的坑,然后从右开始遍历,找到第一个值小于关键字的元素将其填到坑中,则第一个值小于关键字原所在的数组位置是一个新的坑,以此类推。
- 实现代码:
// 快速排序挖坑法
#include <iostream>
#include <vector>
using namespace std;
void Swap(int* p, int* q)
{
int tmp = *p;
*p = *q;
*q = tmp;
}
int GetMidIndex(vector<int>& arr, int left, int right)
{
int mid = left + (right - left) / 2;
if (arr[left] < arr[mid])
{
if (arr[mid] < arr[right])
return mid;
else if (arr[left] > arr[right])
return left;
else
return right;
}
else
{
if (arr[mid] > arr[right])
return mid;
else if (arr[left] < arr[right])
return left;
else
return right;
}
}
int PartSort2(vector<int>& arr, int left, int right)
{
//加上三数取中法来优化
int mid = GetMidIndex(arr, left, right);
Swap(&arr[mid], &arr[right]);
int key = arr[right];
int key_index = right;
while (left < right)
{
while (left < right && arr[left] <= key)
++left;
arr[right] = arr[left];
while (left < right && arr[right] >= key)
--right;
arr[left] = arr[right];
}
arr[left] = key;
return left;
}
void QuickSort2(vector<int>& arr, int left, int right)
{
if (left >= right)
return;
int index = PartSort2(arr, left, right);
QuickSort2(arr, left, index - 1);
QuickSort2(arr, index + 1, right);
}
int main()
{
vector<int> vc = { 7, 4, 5, 9, 8, 2, 1 };
QuickSort2(vc, 0, vc.size() - 1);
for (auto e : vc)
{
cout << e << " ";
}
cout << endl;
return 0;
}
(3.)前后指针法(前找小):
- 实现代码:
// 快速排序前后指针法
#include <iostream>
#include <vector>
using namespace std;
void Swap(int* p, int* q)
{
int tmp = *p;
*p = *q;
*q = tmp;
}
int GetMidIndex(vector<int>& arr, int left, int right)
{
int mid = left + (right - left) / 2;
if (arr[left] < arr[mid])
{
if (arr[mid] < arr[right])
return mid;
else if (arr[left] > arr[right])
return left;
else
return right;
}
else
{
if (arr[mid] > arr[right])
return mid;
else if (arr[left] < arr[right])
return left;
else
return right;
}
}
int PartSort3(vector<int>& arr, int left, int right)
{
//加上三数取中法来优化
int mid = GetMidIndex(arr, left, right);
Swap(&arr[mid], &arr[right]);
int cur = left;
int prev = left - 1;
int key = arr[right];
while (cur < right)
{
if (arr[cur] < key && ++prev != cur)
Swap(&arr[prev], &arr[cur]);
++cur;
}
++prev;
Swap(&arr[right], &arr[prev]);
return prev;
}
void QuickSort3(vector<int>& arr, int left, int right)
{
if (left >= right)
return;
int index = PartSort3(arr, left, right);
QuickSort3(arr, left, index - 1);
QuickSort3(arr, index + 1, right);
}
int main()
{
vector<int> vc = { 7, 4, 5, 9, 8, 2, 1 };
QuickSort3(vc, 0, vc.size() - 1);
for (auto e : vc)
{
cout << e << " ";
}
cout << endl;
return 0;
}
- 快速排序的特性:
(1.)快速排序整体的综合性能和使用场景是比较好的。
(2.)时间复杂度:O(N * logN)
(3.)空间复杂度:O(logN)
(4.)稳定性:不稳定
7.归并排序
-
基本思想:归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个营养品。将已经有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列间有序,若将两个有序表合并成一个有序表,称为二路归并。
-
实现代码:
(1.)递归版
// 归并排序实现代码
// 递归
#include <iostream>
#include <vector>
using namespace std;
void _MergeSort(int* arr, int left, int right, int* tmp)
{
if (left == right)
return;
int mid = left + (right - left) / 2;
_MergeSort(arr, left, mid, tmp);
_MergeSort(arr, mid + 1, right, tmp);
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
int i = left;
while (begin1 <= end1 && begin2 <= end2)
{
if (arr[begin1] <= arr[begin2])
{
tmp[i++] = arr[begin1];
++begin1;
}
else
{
tmp[i++] = arr[begin2];
++begin2;
}
}
while (begin1 <= end1)
{
tmp[i++] = arr[begin1];
++begin1;
}
while (begin2 <= end2)
{
tmp[i++] = arr[begin2];
++begin2;
}
memcpy(arr + left, tmp + left, sizeof(int) * (i - left));
}
void MergeSort(int* arr, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
_MergeSort(arr, 0, n - 1, tmp);
free(tmp);
}
int main()
{
int vc[] = { 7, 4, 5, 9, 8, 2, 1 };
MergeSort(vc, 7);
for (auto e : vc)
{
cout << e << " ";
}
cout << endl;
return 0;
}
(2.)非递归版
// 归并排序实现代码
// 非递归
#include <iostream>
#include <vector>
using namespace std;
void MergeSort(int* arr, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
int gap = 1;
while (gap < n)
{
int i = 0;
for (int begin = 0; begin < n; begin += gap * 2)
{
int begin1 = begin, end1 = begin1 + gap - 1;
if (end1 > n - 1)
end1 = n - 1;
int begin2 = begin + gap, end2 = begin2 + gap - 1;
if (end2 > n - 1)
end2 = n - 1;
while (begin1 <= end1 && begin2 <= end2)
{
if (arr[begin1] < arr[begin2])
{
tmp[i++] = arr[begin1];
++begin1;
}
else
{
tmp[i++] = arr[begin2];
++begin2;
}
}
while (begin1 <= end1)
{
tmp[i++] = arr[begin1];
++begin1;
}
while (begin2 <= end2)
{
tmp[i++] = arr[begin2];
++begin2;
}
}
gap *= 2;
memcpy(arr, tmp, sizeof(int) * n);
}
}
int main()
{
int vc[] = { 7, 4, 5, 9, 8, 2, 1 };
MergeSort(vc, 7);
for (auto e : vc)
{
cout << e << " ";
}
cout << endl;
return 0;
}
- 归并排序的特性:
(1.)归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
(2.)时间复杂度:O(N * logN)
(3.)空间复杂度:O(N)
(4.)稳定性:稳定
8.计数排序
-
基本思想:计数排序又称鸽巢原理,是对哈希直接定址法的变形应用。
-
实现代码:
// 计数排序实现代码
#include <iostream>
#include <vector>
using namespace std;
void CountSort(vector<int>& arr, int n)
{
int min = arr[0], max = arr[0];
for (int i = 0; i < n; ++i)
{
if (min > arr[i])
min = arr[i];
if (max < arr[i])
max = arr[i];
}
int range = max - min + 1;
int* tmp = (int*)malloc(sizeof(int) * range);
memset(tmp, 0, sizeof(int) * range);
// tmp为计数数组C
for (int i = 0; i < n; ++i)
{
++tmp[arr[i] - min];
}
int j = 0;
for (int i = 0; i < range; ++i)
{
while (tmp[i]--)
arr[j++] = min + i;// 重置数组,是原始数组变为数组B
}
}
int main()
{
vector<int> vc = { 7, 4, 5, 9, 8, 2, 1 };
CountSort(vc, vc.size());
for (auto e : vc)
{
cout << e << " ";
}
cout << endl;
return 0;
}
- 计数排序的特性:
(1.)计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
(2.)时间复杂度:O(MAX(N, 范围))
(3.)空间复杂度:O(范围)
(4.)稳定性:稳定
9.总结
排序方法 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
直接插入排序 | O(N ^ 2) | O(N) | O(N ^2) | O(1) | 稳定 |
希尔排序 | O(N * logN) - O(N ^ 2) | O(N ^ 1.3) | O(N ^2) | O(1) | 不稳定 |
直接选择排序 | O(N ^ 2) | O(N ^ 2) | O(N ^2) | O(1) | 稳定 |
堆排序 | O(N * logN) | O(N * logN) | O(N * logN) | O(1) | 不稳定 |
冒泡排序 | O(N ^ 2) | O(N) | O(N ^2) | O(1) | 稳定 |
快速排序 | O(N * logN) | O(N * logN) | O(N ^ 2) | O(logN) - O(N) | 不稳定 |
归并排序 | O(N * logN) | O(N * logN) | O(N * logN) | O(N) | 稳定 |