这期来说一说排序,排序在程序中十分重要,虽然各种高级编程语言中都已经有实现好的排序方法,但是我们也应该知道其中的各种排序的实现原理。
直接插入排序
直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
- 元素集合越接近有序,直接插入排序算法的时间效率越高
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
void InsertSort(int* a, int n) {
assert(a);
int i;
for (i = 0; i < n - 1; ++i) {
//单个元素的排序
//找到已经排好序的最后一个元素的位置
int end = i;
//把end+1位置的元素插到合适的位置
int tmp = a[end + 1];
while (end >= 0 && a[end] > tmp) {
a[end + 1] = a[end];
--end;
}
//找到合适的位置
a[end + 1] = tmp;
}
}
希尔排序
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。
- 希尔排序是对直接插入排序的优化。
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
- 希尔排序的时间复杂度不好计算,需要进行推导,推导出来平均时间复杂度: O(N^1.3 — N^2)
- 稳定性:不稳定
void ShellSort(int *a, int n) {
int gap = n;
int i;
while (gap > 1) {
//gap > 1 --> 预排序过程
//gap = 1 -->排序过程
gap = gap / 3 + 1; //保证最后一次gap为1的插入排序
for (i = 0; i < n - gap; ++i) { //交错的排序
int end = i;
int tmp = a[end + gap];
while (end >= 0 && a[end] > tmp) {
a[end + gap] = a[end];
end -= gap;
}
a[end + gap] = tmp;
}
}
}
简单选择排序
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定(可以变成稳定)
void selectSort(int* a, int n) {
int begin = 0, end = n - 1;
while (begin < end) {
//每次选一个最大的和最小的,放到相应的位置
int i, max, min;
max = min = begin;
//小的选第一个,大的选最后一个
for (i = begin; i <= end; ++i) {
if (a[i] < a[min])
min = i;
if (a[i] >= a[max])
max = i;
}
//min--->begin max--->end
swap(&a[begin], &a[min]);
//判断最大元素的位置是否发生变化
if (max == begin)
max = min;
swap(&a[end], &a[max]);
begin++;
--end;
}
}
堆排序
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
- 堆排序使用堆来选数,效率就高了很多。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(1)
- 稳定性:不稳定
void ShiftDown(int* a, int n, int root) {
int child = root * 2 + 1;
int parent = root;
while (child < n) {
if (child + 1 < n) {
if (a[child] < a[child + 1]) {
child += 1;
}
}
if (a[parent] < a[child]) {
int tmp = a[child];
a[child] = a[parent];
a[parent] = tmp;
parent = child;
child = parent * 2 + 1;
} else {
break;
}
}
}
void HeapSort(int* a, int n) {
//建堆,最后一棵子树开始 :(n - 2) / 2
int i, end;
for (i = (n - 2) / 2; i >= 0; --i) {
ShiftDown(a, n, i);
}
//排序:1.swap 2.shiftdown
end = n - 1;
while (end) {
swap(&a[0], &a[end]);
ShiftDown(a, end, 0);
--end;
}
}
冒泡排序
- 冒泡排序是一种非常容易理解的排序
- 时间复杂度:O(N - N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
//优化:加一个标记,如果一趟冒泡没有交换动作,直接跳出
void BubbleSort(int* a, int n) {
int i, j;
for (i = n; i > 0; --i) {
int flag = 0;
for (j = 0; j < i - 1; j++) {
if (a[j] > a[j + 1]) {
swap(&a[j], &a[j + 1]);
flag = 1;
}
}
if (flag == 0)
break;
}
}
快速排序
其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
这里将对其进行**优化 **:
1.三数取中法:当待排序列有序时,普通快排时间复杂度将提到N^2,这里将从始端、尾端及中间位置选取中间值为基准值来避免这种情况。
//三数取中法
int getMid(int* a, int left, int right) {
int mid = left + (right - left) / 2;
if (a[mid] > a[left]) {
if (a[mid] < a[right])
return mid;
else {
//mid > left,right
if (a[left] > a[right])
return left;
else
return right;
}
} else {
if (a[left] < a[right])
return left;
else {
//left >= right , mid
if (a[mid] > a[right])
return mid;
else
return right;
}
}
}
2.小区间优化:在小区间不调用递归,可以使用插排等其他排序提升效率
void QuickSort(int* a, int left, int right) {
if (left > right) {
return;
}
//小区间优化:小区间不调用递归
else if (right - left + 1 < 5) {
insertSort(a + left, right - left + 1);
} else {
int mid = partQuickSort1(a, left, right);
//int mid = partQuickSort2(a, left, right);
//int mid = partQuickSort3(a, left, right);
QuickSort(a, left, mid - 1);
QuickSort(a, mid + 1, right);
}
}
将区间按照基准值划分为左右两半部分的常见方式有:
1.hoare版本
int partQuickSort1(int* a, int left, int right) {
//三数取中
int mid = getMid(a, left, right);
swap(&a[left], &a[mid]);
int key = a[left];
int start = left;
//寻找大小元素交换
while (left < right) {
//先从右边找小于key的
while (left < right && a[right] >= key)
--right;
//从左边找大于key的值
while (left < right && a[left] <= key)
++left;
swap(&a[left], &a[right]);
}
//key的位置确定:左右相遇的位置
swap(&a[start], &a[left]);
return left;
}
2.挖坑法
int partQuickSort2(int* a, int left, int right) {
//三数取中
int mid = getMid(a, left, right);
swap(&a[mid], &a[left]);
int key = a[left];
while (left < right) {
//从右边找小
while (left < right && a[right] >= key)
--right;
//填坑
a[left] = a[right];
//从左找大的
while (left < right && a[left] <= key)
++left;
//填坑
a[right] = a[left];
}
//存放key
a[left] = key;
return key;
}
3.前后指针版本
int partQuickSort3(int* a, int left, int right) {
//三数取中
int mid = getMid(a, left, right);
swap(&a[mid], &a[left]);
//最后一个小于key的位置
int prev = left;
//下一个小于key的位置
int cur = left + 1;
int key = a[left];
while (cur <= right) {
//如果下一个小于key的位置于上一个小于key的位置不连续
//说明中间有大于key的值,进行交换,大-->向后移动,小 <-- 向前移动
if (a[cur] < key && ++prev != cur) {
swap(&a[prev], &a[cur]);
}
++cur;
}
swap(&a[prev], &a[left]);
return prev;
}
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
- 平均时间复杂度:O(N*logN)
- 空间复杂度:O(logN)
- 稳定性:不稳定
快排非递归
void QuickSortNoR(int* a, int left, int right) {
stack<int> st;
if (left < right) {
st.push(right);
st.push(left);
}
while (!st.empty()) {
int begin = st.top();
st.pop();
int end = st.top();
st.pop();
//划分当前区间
int mid = partQuickSort1(a, begin, end);
//划分左右子区间
if (begin < mid - 1) {
st.push(mid - 1);
st.push(begin);
}
if (mid + 1 < end) {
st.push(end);
st.push(mid + 1);
}
}
}
归并排序
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
//时间:N * logN
//空间:N + logN —— N
void _mergeSort(int* a, int left, int right, int* tmp) {
if (left >= right) {
return;
}
//分解
int mid = left + (right - left) / 2;
_mergeSort(a, left, mid, tmp);
_mergeSort(a, mid + 1, right, tmp);
//归并
int begin1 = left, end1 = mid,
begin2 = mid + 1, end2 = right;
int tmpindex = begin1;
while (begin1 <= end1 && begin2 <= end2) {
if (a[begin1] <= a[begin2])
tmp[tmpindex++] = a[begin1++];
else
tmp[tmpindex++] = a[begin2++];
}
while (begin1 <= end1)
tmp[tmpindex++] = a[begin1++];
while (begin2 <= end2)
tmp[tmpindex++] = a[begin2++];
//拷贝到原有数组对应区间
memcpy(a + left, tmp + left, (right - left + 1) * sizeof(int));
}
void mergeSort(int* a, int n) {
int* tmp = (int*)malloc(sizeof(int) * n);
_mergeSort(a, 0, n - 1, tmp);
free(tmp);
}
计数排序
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:
- 统计相同元素出现次数
- 根据统计的结果将序列回收到原来的序列中
- 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
- 时间复杂度:O(MAX(N,范围))
- 空间复杂度:O(范围)
- 稳定性:稳定
void countSort(int* a, int n) {
//范围:最小值——最大值
int min = a[0], max = a[0];
int i;
//获取数据的范围
for (i = 1; i < n; ++i) {
if (a[i] < min)
min = a[i];
if (a[i] > max)
max = a[i];
}
int range = max - min + 1;
int* countArr = (int*)malloc(sizeof(int) * range);
memset(countArr, 0, sizeof(int) * range);
//计数
for (i = 0; i < n; ++i) {
countArr[a[i] - min]++;
}
//排序
int index = 0;
for (i = 0; i < range; ++i) {
while (countArr[i]--) {
a[index++] = i + min;
}
}
}