排序的概念
排序:就是将数据进行递增或者递减的操作。
还有一个稳定性的概念:在一组不有序的数组中,有两个相同的数据,排完序之后,左边那个依然在左边,右边那个依然在右边。
举例:
在一场考试中,如果有3个人成绩是一样的,那么对于他们三个的成绩排名将会根据交卷时间快慢进行判定谁先谁后。
常见的排序算法
插入排序
直接插入排序
这个排序的思想就是将数据一个一个插入到已经有序的数组中(只有一个数据的数组也算有序)。
所以这个算法就很容易实现出来:
void InsertSort(int* a, int n)
{
for (int i = 0; i < n - 1; ++i)
{
int end = i;
int tmp = a[end + 1];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + 1] = a[end];
--end;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
我们来看看它的时间复杂度:
遍历每一个数据,每遍历一次都要和前面的数据进行比较,最快就是尾插,最坏就是到最前面,所以,最好的情况时间复杂度为O(n),最坏的情况时间复杂度就是O(n2 )。
空间复杂度:这个不难理解,就是O(1)。
稳定性:稳定。
希尔排序
这个排序思想就是:先选定一个整数gap,然后,在这个数组中从下标0开始,以n为距离,比如,i(下标) + gap,i(下标) + 2 * gap,一直到遍历完这个数组,把这些数据分为一组进行排序,然后减小gap,重复之前的动作,直到n变成1,此时,数组已经接近有序了,再排一次就有序了。
实现如下:
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)//这里不能等于,如果加上等于,会造成死循环。
{
gap = gap / 3 + 1;//这里注意:这个gap最后必须是1,如果是gap = gap / 2,这样的话可能除到0,所以,我们采用gap = gap / 3 + 1
for (int i = 0; i < n - gap; ++i)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
时间的复杂度:它的时间复杂度不好算,gap是自己定的,上面那个减少gap的方法是Knuth提出的,他在《计算机程序设计技巧》中进行大量的计算,得出时间复杂度在O(n1.25 )~ O(1.6n1.25 )。
空间复杂度:O(1 )。
稳定性:不稳定。
选择排序
直接选择排序
这个排序思想:选出左右两边的下标,遍历一遍数组,从而选出最大值和最小值,把最小值和左下标进行交换,最大值和右下标进行交换,然后左下标 ++,右下标 --。
实现如下:
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void SelectSort(int* a, int n)
{
int begin = 0;
int end = n - 1;
while (begin < end)
{
int min = begin;
int max = begin;
for (int i = begin + 1; i <= end; ++i)
{
if (a[i] < a[min])
{
min = i;
}
if (a[i] > a[max])
{
max = i;
}
}
Swap(&a[begin], &a[min]);
if (max == begin)
max = min;
Swap(&a[end], &a[max]);
begin++;
end--;
}
}
时间复杂度:O(n2)。
空间复杂度:O(1)。
稳定性:不稳定。
堆排序
它是利用堆这种数据结构来设计的一种堆排序,用它排升序就用大推,排降序就用小堆。
实现:
void AdjustDwon(int* a, int n, int root)
{
int child = root * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child] < a[child + 1])
{
child++;
}
if (a[root] < a[child])
{
Swap(&a[root], &a[child]);
root = child;
child = root * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int* a, int n)
{
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDwon(a, n, i);
}
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDwon(a, end, 0);
--end;
}
}
时间复杂度:O(N*logN)。
空间复杂度:O(1)。
稳定性:不稳定
交换排序
冒泡排序
冒泡排序其实就是模拟实现qsort:
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n; ++i)
{
int flag = 0;
for (int j = 1; j < n - i; ++j)
{
if (a[j - 1] > a[j])
{
Swap(&a[j - 1], &a[j]);
flag = 1;
}
}
if (flag == 0)
{
break;
}
}
}
时间复杂度:O(N2)。
空间复杂度:O(1)。
稳定性:稳定。
快速排序
这个排序算法的核心思想主要就是在数组中最左边或最右边选一个数作key,每一趟都把key回归到最终应该出现的位置,然后key的左边为一个数组,右边为一个数组,继续之前的操作,直到递归到只剩一个数的时候(一个数就表示已经是有序的了),继续递归,完了之后这整个数组就都是有序的了。
而上面这个思路共有3个方法:
Hoare版本
这个方法是假设把最左边的值作为key,然后,先右边开始遍历,遇到比key小的就停下,再左边开始遍历,遇到比key大的就停下然后左右两边开始交换,然后重复之前的操作,直到右下标遇到左下标就停下,此时,停下的位置就是在key在排完序之后最终的位置。
int GetmidIndex(int* a, int begin, int end)//三数取中
{
int mid = abs(end - begin) / 2 + begin;
if (a[begin] < a[mid])
{
if (a[mid] < a[end])
{
return mid;
}
else if (a[end] < a[begin])
{
return begin;
}
else
{
return end;
}
}
else
{
if (a[mid] > a[end])
{
return mid;
}
else if (a[begin] < a[end])
{
return begin;
}
else
{
return end;
}
}
}
int PartSort1(int* a, int left, int right)
{
int mid = GetmidIndex(a, left, right);
Swap(&a[left], &a[mid]);
int begin = left;
int end = right;
int key = begin;
while (begin < end)
{
while (begin < end && a[end] >= a[key])
{
--end;
}
while (begin < end && a[begin] <= a[key])
{
++begin;
}
Swap(&a[begin], &a[end]);
}
Swap(&a[begin], &a[key]);
key = begin;
return key;
}
挖坑法
其实这个方法和上面的Hoare方法差不多
int PartSort2(int* a, int left, int right)
{
int mid = GetmidIndex(a, left, right);
Swap(&a[left], &a[mid]);
int begin = left;
int end = right;
int key = a[begin];
int hole = left;
while (begin < end)
{
while (begin < end && a[end] >= key)
{
--end;
}
a[hole] = a[end];
hole = end;
while (begin < end && a[begin] <= key)
{
++begin;
}
a[hole] = a[begin];
hole = begin;
}
a[hole] = key;
return hole;
}
左右指针
这个方法的思想是以最左边的值为key,用一个指针cur从起点的下一个位置出发,去前面探路,还有一个指针prev从起点出发,只要cur探路发现有比key小的就交换prev的下一个位置(因为prev在cur后面,并且不动,所以永远指向比key小的数据,所以要先++再交换)
int PartSort3(int* a, int left, int right)
{
int key = left;
int prev = left;
int cur = left + 1;
while (cur <= right)
{
if (cur <= right && a[cur] < a[key])
{
prev++;
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[prev], &a[key]);
key = prev;
return key;
}
核心代码搞清楚之后,我们就可以把整个排序给写出来了:
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
if (right - left + 1 < 15)
{
InsertSort(a + left, right - left + 1);//如果排序量少的话,可以直接用插入排序。
}
else
{
int key = PartSort1(a, left, right);
//int key = PartSort2(a, left, right);
//int key = PartSort3(a, left, right);
QuickSort(a, left, key - 1);
QuickSort(a, key + 1, right);
}
}
时间复杂度:O(N*logN)。
空间复杂度:O(logN ) ~ O(N)。
稳定性:不稳定。
归并排序
归并排序
这个算法用的递归的思想,先把左边的数组排成有序,再把后边的数组排成有序,最后再把左右两边合并到一起,就形成有序的数组。
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin >= end)
{
return;
}
//int mid = abs(end - begin) / 2 + begin;
int mid = (begin + end) / 2;
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
memcpy(a + begin, tmp + begin,sizeof(int)* (end - begin + 1));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
tmp = NULL;
}
时间复杂度:O(N*logN)。
空间空间复杂度:O(N)。
稳定性:稳定。
补充
还有一种非比较排序,也称计数排序。
它是先创建一个新的数组,然后统计数组中每个数出现的次数,放在这个新的数组中。
void CountSort(int* array, int sz)
{
int max = array[0];
int min = array[0];
for (int i = 0; i < sz; ++i)
{
if (array[i] < min)
{
min = array[i];
}
if(array[i] > max)
{
max = array[i];
}
}
int range = max - min + 1;
int* arr = (int*)calloc(range, sizeof(int));
if (arr == NULL)
{
perror("calloc fail");
exit(-1);
}
for (int i = 0; i < sz; ++i)
{
arr[array[i] - min]++;
}
int j = 0;
for (int i = 0; i < range; ++i)
{
int count = arr[i];
while (count--)
{
array[j++] = i + min;
}
}
free(arr);
arr = NULL;
}
它这个排序有一定的局限性,需要数据比较集中,拿空间换时间的排序。
时间复杂度:它的时间复杂度取决于N和开数组的范围哪个大,所以时间复杂度为O(MAX(N,范围))。
空间复杂度:O(范围)。
稳定性:稳定。