目录
1、排序的概念及其运用
1.1 排序的概念
排序:所谓排序,就是使一串记录,按照其中的某个关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
1.2 排序运用
1.3 常见排序算法
以上就是我们常说的七大排序。
2. 常见的排序算法的实现
2.1 插入排序
2.1.1 基本思想:
直接插入排序是一种简单的插入排序法,其基本思想是:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。
实际中我们玩扑克牌时,就用了插入排序的思想
2.1.2 直接插入排序
void InsertSort(int* a, int n) { assert(a); for (int i = 0; i < n - 1; ++i) { int end = i; int x = a[end+1]; while (end >= 0) { if (a[end] > x) { a[end + 1] = a[end]; --end; } else { break; } } a[end + 1] = x; } }
2.2 希尔排序 -- 直接插入排序的思想优化
2.2.1 希尔排序基本思想
1、 分组预排序 -- 数组接近有序
2、 直接插入排序
按gap分组,对分组值进行插入排序,分成gap组,gap>1
2.2.2 时间复杂度
最好:O(N)
最坏:F(N,gap) = (1 + 2 + 3 + ... + N/gap )*gap
gap越大,预排越快,越不接近有序
gap越小,预排更慢,越接近有序
gap == 1 就是直接插入排序
2.2.3 代码实现
void ShellSort(int* a, int n)
{
// 按gap分组数组进行预排序
/*int gap = 3;
for (int j = 0; j < gap; j++)
{
for (int i = j; i < n - gap; i += gap)
{
int end = i;
int x = a[end + gap];
while (end >= 0)
{
if (a[end] > x)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = x;
}
}*/
// 多次预排序(gap > 1) + 直接插入
int gap = n;
while (gap > 1)
{
//gap = gap / 2;
gap = gap / 3 + 1;
// 多组一锅炖
for (int i = 0; i < n - gap; ++i)
{
int end = i;
int x = a[end + gap];
while (end >= 0)
{
if (a[end] > x)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = x;
}
}
}
2.3 选择排序
2.3.1 插入排序的基本思想
选择排序(Selection sort)是一种简单直观的排序算法。 它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。 以此类推,直到全部待排序的数据元素的个数为零。 选择排序是不稳定的排序方法。
2.3.2 代码实现
void Swap(int* px, int* py)
{
int tmp = *px;
*px = *py;
*py = tmp;
}
void SelectSort(int* a, int n)
{
int begin = 0, end = n - 1;
while (begin < end)
{
int maxi = begin;
int mini = begin;
for (int i = begin; i <= end; i++)
{
if (a[i] > a[maxi])
{
maxi = i;
}
if (a[i] < a[mini])
{
mini = i;
}
}
Swap(&a[begin], &a[mini]);
if (maxi == begin)
{
maxi = mini;
}
Swap(&a[end], &a[maxi]);
begin++;
end--;
}
}
2.3.3 时间复杂度
最好:O(N^2) 计算机并不知道它是有序,还是需要遍历一遍才能知道是否有序
最坏:O(N^2)
所以选择排序是整体而言最差的排序,因为无论什么情况都是O(N^2)
2.4 堆排序
2.4.1 堆排序基本思想
首先,我们从最后一个非叶子节点开始,从后往前调整,每次向下调整成大根堆。整个数组变为大根堆的时候,从后往前与首元素交换,将交换后的首元素向下调整,这样循环往复就能得到一个有序队列。
2.4.2 代码实现
void AdjustDown(int* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
//选出左右孩子中小的那一个
if (child + 1 < n && a[child + 1] > a[child])
{
++child;
}
//如果小的孩子小于父亲,则交换,并继续向下调整
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int* a, int n)
{
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}
2.4.3 时间复杂度
在调整为大堆的时候时间复杂度为O(N)
在排序的时候时间复杂度为O(N*logN)
2.5 冒泡排序
2.5.1 冒泡排序基本思想
从第二个元素开始,依次向后,假如前一个值比他大,就交换。如果这一次循环并没有交换数值那么这个数组是有序的,跳出循环
2.5.2 代码实现
void bubble(int* a ,int n)
{
int end = n;
while (end > 0)
{
int exchange = 0;
for (int i = 1; i < n; i++)
{
if (a[i - 1] > a[i])
{
exchange = 1;
Swap(&a[i - 1], &a[i]);
}
}
end--;
if (exchange == 0)
{
break;
}
}
}
2.5.3 时间复杂度
最好情况:O(N)
最坏情况:O(N^2)
2.6 快速排序
2.6.1 快速排序基本思想 hoare版本
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想:任取待排序元素序列中的某元素作为基准值,按照该排序码排序集合分割为两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
2.6.2 单趟排序:
一般选最左边/左右边的值做key
单趟排序的目标:左边的值比key要小,右边的值比key要大
选最右边的值做key,左边先走
选最左边的值做key,右边先走
假如选最右边的值做key,左边先走找比key大的数,找到了右边开始走,找比key小的数,找到后交换位置,直到左边等于右边,之后交换key与最终位置。
不用担心key与最终位置交换后不符合规律。
因为最终假如左边走到了右边的位置,已经交换过右边位置的值一定大于key。
假如右边走到左边的位置,左边一定是找到了比key大的值。
2.6.3 单趟排序代码
void Partion(int* a, int left, int right)
{
int keyi = left;
while (left < right)
{
//右边先走,找小
while (left < right && a[right] >= a[keyi])
{
if (a[right - 1] != NULL)
--right;
}
//之后左边走,找大
while (left < right && a[left] <= a[keyi])
{
++left;
}
Swap(&a[left], &a[right]);
}
Swap(&a[left], &a[keyi]);
}
2.6.4 快排主体
快排的主体运用的思路是递归,与二叉树的递归相似,
将数组分为数部分[left, keyi - 1] keyi [keyi+1, right]
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int keyi = Partion(a, left, right);
//[left, keyi - 1] keyi [keyi+1, right]
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
2.6.5 时间复杂度
假设每次选的key都是中位数 时间复杂度:O(logN)
当数组有序的时候 时间复杂度:O(N^2)
当我们用快排排序大量有序数时,会造成栈溢出
由此我们引出了递归程序的缺陷:
1、相比循环程序,性能差。(针对早期编译器,是这样的,因为对于递归调用,建立栈帧优化不大。现在编译器优化都很好,递归相比循环性能差不了多少)
2、递归程度太深,会导致栈溢出。
2.6.6 如何解决快排面对有序的选ket问题
1、随机选key --- 这样我们的命运就成随机的了
2、三数取中
2.6.7 三数取中 --- 面对有序最坏情况变成最好情况
在面对有序数组时,假设我们快排中的key是最左边的数,那么我们的时间复杂度会变成N^2。这时候我们可以用三数取中方法来进行排序,避免栈溢出和复杂度过大。我们分别取最左边的数left与最右边的数right,之后进行运算取中间数mid。
取中间数mid时要注意,不能int mid = (left + right) / 2;我们要避免超过int最大值。
之后三个进行比较寻找中间值
int GetMidIndex(int* a, int left, int right)
{
//int mid = (left + right) / 2;
int mid = left + (right - left) / 2;
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] > a[right])
{
return left;
}
else
{
return right;
}
}
else
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return left;
}
else
{
return right;
}
}
}
我们在单趟排列中也要修改一下
int Partion(int* a, int left, int right)
{
int mid = GetMidIndex(a, left, right); //先返回的中间值定义
Swap(&a[mid],&a[left]); //之后将他与left交换,这样就相当于二分法,时间复杂度降低
int keyi = left;
while (left < right)
{
//右边先走,找小
while (left < right && a[right] >= a[keyi])
{
if (a[right - 1] != NULL)
--right;
}
//之后左边走,找大
while (left < right && a[left] <= a[keyi])
{
++left;
}
Swap(&a[left], &a[right]);
}
Swap(&a[left], &a[keyi]);
return left;
}
2.7 快排单趟排序的第二种方法 --- 挖坑法
2.7.1 具体思路
我们的大体思路与hoare版本是一致的,因为挖坑法是hoare的变种。
我们可以将最左边的值用key保存,也可以将最右边。
假设我们从最左边开始,定义一个坑位(pivot)的值为left,之后从最右边开始寻找比key小的值,
找到后right与pivot位置中的值交换,pivot变为right,形成新的坑位,完成后,从左边开始找比key大的值,找到后pivot与left位置中的值交换,pivot变为left,形成另一个新坑位。最后将坑位赋值为最开始的left中的值,也就是key,返回值为pivot
2.7.2 代码实现
int Partion2(int* a, int left, int right)
{
int mid = GetMidIndex(a, left, right);
Swap(&a[mid], &a[left]);
int key = a[left];
int pivot = left;
while (left < right)
{
//右边找小 放到左边的坑里
while (left < right && a[right] >= key)
{
--right;
}
a[pivot] = a[right];
pivot = right;
while (left < right && a[left] <= key)
{
++left;
}
a[pivot] = a[left];
while (left < right && a[left] <= key)
{
++left;
}
a[pivot] = a[left];
pivot = left;
}
a[pivot] = key;
return pivot;
}
2.8 快排单趟排序的第三种方法 --- 前后指针法
2.8.1 思路
8 | 1 | 2 | 7 | 9 | 3 | 4 | 5 | 10 | 6 | |
prev | cur | key |
我们将最后一位6作为key,cur指向第一个元素,prev在cur前一位。cur向后寻找比key --- 6小的数,假如找到了,prev++,之后与prev交换。直到cur走到key,最后key的值与prev的值交换。
1 | 8 | 2 | 7 | 9 | 3 | 4 | 5 | 10 | 6 | |
prev | cur | key |
1 | 2 | 8 | 7 | 9 | 3 | 4 | 5 | 10 | 6 | |
prev | cur | key |
1 | 2 | 3 | 7 | 9 | 8 | 4 | 5 | 10 | 6 | |
prev | cur | key |
1 | 2 | 3 | 4 | 9 | 8 | 7 | 5 | 10 | 6 | |
prev | cur | key |
1 | 2 | 3 | 4 | 5 | 8 | 7 | 9 | 10 | 6 | |
prev | cur | key |
1 | 2 | 3 | 4 | 5 | 8 | 7 | 9 | 10 | 6 | |
prev | key cur |
当cur走到key时,交换prev 与 key的值
1 | 2 | 3 | 4 | 6 | 8 | 7 | 9 | 10 | 5 | |
prev | key cur |
2.8.2 代码
int Partion4(int* a, int left, int right)
{
int key = left;
int prev = left;
int cur = prev + 1;
while (cur <= right)
{
if (a[cur] < a[key] && ++prev != cur)
{
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[prev], &a[key]);
return prev;
}
快排有个致命问题,就是一组数列全为同一个数
这个时候快排会非常的慢
2.9 归并排序
2.9.1 思路
假设数组分为左右两部分,左部分有序,右部分也有序,那么可以将他归并为一个有序数组。
需要用到递归,将一个数组拆分为左右数组,直到其中只有一个元素,之后将他排序合并,1合2,2合4,4合8。以此类推,之后合并成一个有序数组。
2.9.2 代码
void _MergeSort(int* a, int left, int right, int* tmp)
{
if (left >= right) //递归条件
{
return;
}
int mid = left + (right - left) / 2; //计算中间值
//要将数组分为[left, mid] [mid + 1, right]两部分
_MergeSort(a, left, mid, tmp); //先分左部分
_MergeSort(a, mid + 1, right, tmp);
int begin1 = left, end1 = mid; //定义begin1 begin2两个指针向后走
int begin2 = mid + 1, end2 = right;
int i = left;
while (begin1 <= end1 && begin2 <= end2) //谁小就将谁放入临时空间tmp中
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1) //不知道谁先走完,这两个while循环中最多有一个会执行
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
//tmp数组拷贝回a
for (int j = left; j <= right; j++)
{
a[j] = tmp[j];
}
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n); //先开辟一块能存放下a数组的空间
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
_MergeSort(a, 0, n - 1, tmp); //将数组传入
free(tmp); //释放数组
tmp = NULL;
}
2.10 基数排序
2.10.1 思路
先将数组中最大值与最小值取出来,最大值与最小值就是这个数组的范围,之后开辟一个与这个范围相同大小的空间。比如{1,0,3,6,5,4,2,1,3},从头开始访问数组,第一个数是1,那么我们新开辟的数组空间叫做count,那么count[1]++,第二次是0,count[0]++。这个步骤称为统计,也就是在count内对应位置统计其出现过的次数。之后将count内的数按照出现次数按照顺序放回原数组,那么我们就排序完成了。
2.10.2 注意事项
假如我们最小数不是0呢?
我们开辟了这个范围大小的数组,但是我们的值不一定在这个范围内,所以我们统计出现次数的时候就可能会越界。打个比方,{5,6,7,8,9} 我们的范围是 [0,9 - 5 +1] 也就是[0,5]。可是当我们将6,7,8,9放进去,没有相应位置,会造成越界。那么我们就需要一个思路,相对映射。
2.10.3 相对映射
我们在统计的时候,将原数组中的数减去最小值,那么我们的数组就是不是从0开始统计的呢。0代表着最小值的位置,那么我们开辟的空间大小就是这个范围内的大小。之后我们在放回原数组时,将其加上最小值,就可以还原原数组的大小了。整体思路就是放进去的时候减去最小值,让数组从0开始,拿出排序的时候加上最小值,让数组值还原。一加一减,值不变。
2.10.4 代码
void CountSort(int* a, int n)
{
int min = a[0];
int max = a[0];
for (int i = 1; i < n; i++)
{
if (a[i] > max)
{
max = a[i];
}
if (a[i] < min)
{
min = a[i];
}
}
int range = max - min + 1;
int* count = (int*)malloc(sizeof(int) * range);
memset(count, 0, sizeof(int) * range);
if (count == NULL)
{
printf("malloc fail\n");
exit(-1);
}
//统计次数
for (int i = 0; i < n; i++)
{
count[a[i] - min]++;
}
//根据次数排序
int j = 0;
for (int i = 0; i < range; i++)
{
while (count[i]--)
{
a[j++] = i + min;
}
}
free(count);
count = NULL;
}