文章目录
排序的概念
排序: 所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性: 假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序: 数据元素全部放在内存中的排序。
外部排序: 数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
常见的几种排序
插入排序
直接插入排序是一种简单的插入排序法,其基本思想是:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为
止,得到一个新的有序序列 。
实际中我们玩扑克牌时,就用了插入排序的思想
直接插入排序
// 插入排序
//a代表数组 n表示数组元素个数
void InsertSort(int* a, int n)
{
int i = 0;
for (i = 0; i < n-1; i++)
{
int end = a[i+1];//要插入的数据
int temp = i;
while (temp >= 0)
{
if (end < a[temp])
{
a[temp + 1] = a[temp];
temp--;
}
else
{
break;//找到找小于等于插入元素的跳出循环
}
}
a[temp + 1] = end;//交换
}
}
直接插入排序的特性总结:
- 元素集合越接近有序,直接插入排序算法的时间效率越高
- 时间复杂度:O(N^2)
- 空间复杂度:O(1),它是一种稳定的排序算法
- 稳定性:稳定
希尔排序
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。
希尔排序的特性总结:
- 希尔排序是对直接插入排序的优化。
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就
会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。 - 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定因为咋们的gap是按照Knuth提出的方式取值的,而且Knuth进行了大量的试验统计,我们暂时就按照: O ( n 1.24 ) O(n^{1.24}) O(n1.24)到 O ( 1.6 ∗ n 1.25 ) O(1.6*n^{1.25}) O(1.6∗n1.25) 来算。
- 稳定性:不稳定
// 希尔排序
//a代表数组 n表示数组元素个数
void ShellSort(int* a, int n)
{
int gap = n;
// gap > 1 预排序
// gap == 1 直接插入排序
while (gap > 1)
{
//当间距为1时数据已经接近有序,gap=1时就是插入排序
gap = gap / 3 + 1;//这样写才能在大于1之前最后一次循环的间距为1,进行插入排序。
int i = 0;
for (i = 0; i < n - gap; i++)
{
int end = a[i + gap];
int temp = i;
while (temp >= 0)
{
if (end < a[temp])
{
a[temp + gap] = a[temp];
temp -= gap;
}
else
{
break;
}
}
a[temp + gap] = end;
}
}
}
选择排序
基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
直接选择排序
//交换
static void Swap(int* p1, int* p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
// 选择排序
//a代表数组 n表示数组元素个数
void SelectSort(int* a, int n)
{
int left = 0;
int right = n - 1;
while (left < right)
{
int i = 0;
int small = left;
int big = left;
//a[small]可以不加入循环中
for (i = left+1; i <= right; i++)
{
//找小
if (a[small] > a[i])
{
small = i;
}
//找大
if (a[big] < a[i])
{
big = i;
}
}
Swap(&a[left], &a[small]);
if (left == big)//防止left位置是最大的数
{
big = small;
}
Swap(&a[right], &a[big]);
left++;
right--;
}
}
直接选择排序的特性总结:
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
堆排序
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
//向下调整
static void AdjustDwon(int* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
// 确认child指向大的那个孩子
//防止越界
if (child + 1 < n && a[child + 1] > a[child])
{
child += 1;
}
// 1、孩子大于父亲,交换,继续向下调整
// 2、孩子小于父亲,则调整结束
if (a[parent] < a[child])
{
Swap(&a[parent], &a[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
// 堆排序
//a代表数组 n表示数组元素个数
void HeapSort(int* a, int n)
{
int i = 0;
//建堆
//i要等于0,堆顶也需要调整
for (i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDwon(a, n, i);
}
//堆排序
int big = n - 1;
while (big > 0)
{
Swap(&a[0], &a[big]);
AdjustDwon(a, big, 0);
big--;
}
}
直接选择排序的特性总结:
- 堆排序使用堆来选数,效率就高了很多。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(1)
- 稳定性:不稳定
交换排序
基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
冒泡排序
一趟冒泡排序
//交换
static void Swap(int* p1, int* p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
// 冒泡排序
//a代表数组 n表示数组元素个数
void BubbleSort(int* a, int n)
{
int i = 0;
for (i = 0; i < n; i++)
{
int j = 0;
int sort = 0;
for (j = 1; j < n - i; j++)
{
if (a[j] < a[j - 1])
{
Swap(&a[j], &a[j - 1]);
sort = 1;
}
}
// 一趟冒泡过程中,没有发生交换,说明已经有序了,不需要再处理
if (sort == 0)
{
break;
}
}
}
冒泡排序的特性总结:
- 冒泡排序是一种非常容易理解的排序
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
快速排序
**快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:**任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
hoare版本单排
//交换
static void Swap(int* p1, int* p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
static int HoareSort(int* a, int left, int right)
{
int small = left;
int big = right;
int key = small;
while (small < big)
{
//右找小
while (small < big && a[key] <= a[big])//防止越界
{
big--;
}
//左找大
while (small < big && a[key] >= a[small])//防止越界
{
small++;
}
Swap(&a[big], &a[small]);
}
Swap(&a[key], &a[small]);
key = small;
return key;
}
总结
单趟排序:
左边比key要小,右边比key要大.
左边做key,右边先走 ,右边做key,左边先走.
左边做key,右边先走,保证相遇位置。比Key要小
1、相遇,一种是R停住的,L遇到R,相遇位置就是R停住的位置.
1、相遇,一种是L停住的,R遇到L,相遇位置就是L停住的位置.
挖坑法单排
static int HoleSort(int* a, int left, int right)
{
int begin = left;
int end = right;
int key = a[left];
int hole = left;
while (begin < end)
{
//右找小
while (begin < end && key <= a[end])
{
--end;
}
a[hole] = a[end];
hole = end;
//左找大
while (begin < end && key >= a[begin])
{
++begin;
}
a[hole] = a[begin];
hole = begin;
}
a[hole] = key;
return hole;
}
前后指针版本单排
//前后指针法
static int FrontBackSort(int* a, int left, int right)
{
int front = left+1;
int back = left;
int key = left;
while (front <= right)
{
// 找到比key小的值时,跟++back位置交换,小的往前翻,大的往后翻
if (a[key] > a[front] && ++back != front)//如果back与front一样大都不用交
{
Swap(&a[back], &a[front]);
}
++front;
}
Swap(&a[back], &a[key]);
//key = back;
return back;
}
两路化分排序
快排的优化:
- 三数取中,防止要排列的数据原本就是接近有序的或是有序的,每次 Key 的值就在整个数据的最左边或最右边,那莫快排就变成了冒泡排序了时间复杂度就接近 n 2 n^2 n2了。
- 小区间优化,首先我们知道计算机的运行速度是很快的,对10个元素的数组,进行插入排序和快排是没有什么区别的,反而在进行递归版快排时如果要排列的数据非常的多时,这里小于10个元素排序的递归量是非常大的,而且还会栈溢出的,因此在小于10个元素进行排序时我们可以进行插入排序。
递归版
//三数取中间大小的数
int GetMiddle(int* a, int left, int right)
{
int middle = left + (right - left) / 2;
if (a[left] < a[middle])
{
if (a[left] > a[right])
{
return left;
}
else if (a[middle] < a[right])
{
return middle;
}
else
{
return right;
}
}
else //a[left] > a[middle]
{
if (a[middle] > a[right])
{
return middle;
}
else if (a[right] > a[left])
{
return left;
}
else
{
return right;
}
}
}
void PartSort(int* a, int left, int right)
{
// >表示越界了 = 只剩一个数据了,那个数据默认是有序的
if (left >= right)
{
return;
}
if ((right - left + 1) < 10)
{
// 小区间用直接插入替代,减少递归调用次数
InsertSort(a + left, right - left + 1);//因为插入排序是开区间,lef right是闭区间所以要(right - left + 1)加1
}
else
{
//防止出现接近有序的顺序逆序数组
int middle = GetMiddle(a, left, right);
Swap(&a[middle], &a[left]);
//int key = HoareSort(a, left, right);//hoare版本
//int key = HoleSort(a, left, right);//挖坑法
int key = FrontBackSort(a, left, right);//前后指针法
//数组分成了三部分
//[left,key-1]key[key+1,right]
PartSort(a, left, key - 1);
PartSort(a, key + 1, right);
}
}
非递归版
画个图用栈实现快排的非递归版就非常好理解了。
//非递归实现快排
void StackPartSort(int* a, int left, int right)
{
ST p1;
StackInitialize(&p1);
StackPush(&p1, right);
StackPush(&p1, left);
while (!StackEmpty(&p1))
{
int begin = StackTop(&p1);
StackPop(&p1);
int end = StackTop(&p1);
StackPop(&p1);
//防止出现接近有序的顺序逆序数组
int middle = GetMiddle(a, left, right);
Swap(&a[middle], &a[begin]);
//int key = HoareSort(a, left, right);//hoare版本
//int key = HoleSort(a, left, right);//挖坑法
int key = FrontBackSort(a, begin, end);//前后指针法
if (key + 1 < end)
{
StackPush(&p1, end);
StackPush(&p1, key + 1);
}
if (begin < key - 1)
{
StackPush(&p1, key - 1);
StackPush(&p1, begin);
}
}
StackDestroy(&p1);
}
三路划分排序
实现原理
核心思想总结:
- 跟key相等的值往后推。
- 比key大的甩到右边
- 比key小的甩到左边
这时和key相等的就集中在一起,这种方法对有很多数据相同数列排序的很快。
划分成三部分。
void PartSort(int* a, int left, int right)
{
// >表示越界了 = 只剩一个数据了,那个数据默认是有序的
if (left >= right)
{
return;
}
if ((right - left + 1) < 10)
{
// 小区间用直接插入替代,减少递归调用次数
InsertSort(a + left, right - left + 1);//因为插入排序是开区间,lef right是闭区间所以要(right - left + 1)加1
}
else
{
//防止出现接近有序的顺序逆序数组
/*int middle = GetMiddle(a, left, right);
Swap(&a[middle], &a[left]);*/
int begin = left;
int end = right;
int key = a[left];
int cur = left + 1;
while (cur <= end)
{
if (key > a[cur])
{
Swap(&a[begin], &a[cur]);
++begin;
++cur;
}
else if (key < a[cur])
{
Swap(&a[cur], &a[end]);
--end;
}
else
{
++cur;
}
}
PartSort(a, left, begin - 1);
PartSort(a, end+1 , right);
}
}
快速排序的特性总结:
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
- 时间复杂度:O(N*logN)
- 空间复杂度:O(logN)
- 稳定性:不稳定
归并排序
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and
Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有
序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:
递归版
//交换
static void Swap(int* p1, int* p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
void _MergeSort(int* a, int left, int right, int* array)
{
//采用后序遍历
if (left >= right)//防止递归到最后数据大小出错,递归到只有一个数据是也返回,因为一个数据默认是有顺序的
{
return;
}
int middle = left + (right - left) / 2;
// [left,middle] [middle+1, right] 递归让子区间有序
_MergeSort(a, left, middle, array);
_MergeSort(a, middle + 1, right, array);
int begin1 = left, end1 = middle;
int begin2 = middle+1, end2 = right;
int i = left;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
array[i++] = a[begin1++];
}
else
{
array[i++] = a[begin2++];
}
}
//防止前半部分或后半部分还剩下的有数据
while (begin1 <= end1)
{
array[i++] = a[begin1++];
}
while (begin2 <= end2)
{
array[i++] = a[begin2++];
}
memcpy(a + left, array + left, sizeof(int) * (right - left + 1));
}
void MergeSort(int* a, int n)
{
int* array = (int*)malloc(sizeof(int) * n);
if (NULL == array)
{
perror("MergeSort::temp");
exit(-1);
}
_MergeSort(a, 0, n - 1, array);
}
非递归版
越界问题
-
end1 begin2 end2越界
-
begin2 end2越界
-
end2越界
不修改区间
void MergeSortNonR1(int* a, int n)
{
int* array = (int*)malloc(sizeof(int) * n);
if (NULL == array)
{
perror("MergeSort::temp");
exit(-1);
}
// 归并每组数据个数,从1开始,因为1个认为是有序的,可以直接归并
int gap = 1;//间距
while (gap < n)
{
for (int j = 0; j < n; j += 2 * gap)
{
int begin1 = j, end1 = j+gap-1;
//int begin2 =j+gap, end2 = j+2*gap-1;
int begin2 =end1+1, end2 = begin2+gap-1;
int i = j;
if (end1 >= n)
{
break;
}
else if (begin2 >= n)
{
break;
}
else if (end2 >= n)
{
end2 = n - 1;
}
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
array[i++] = a[begin1++];
}
else
{
array[i++] = a[begin2++];
}
}
//防止前半部分或后半部分还剩下的有数据
while (begin1 <= end1)
{
array[i++] = a[begin1++];
}
while (begin2 <= end2)
{
array[i++] = a[begin2++];
}
// 归并一部分,拷贝一部分
memcpy(a + j, array + j, sizeof(int) * (end2 - j + 1));
}
gap *= 2;
}
free(array);
array = NULL;
}
修改区间
void MergeSortNonR2(int* a, int n)
{
int* array = (int*)malloc(sizeof(int) * n);
if (NULL == array)
{
perror("MergeSort::temp");
exit(-1);
}
// 归并每组数据个数,从1开始,因为1个认为是有序的,可以直接归并
int gap = 1;//间距
while (gap < n)
{
int j = 0;
for (j = 0; j < n; j += 2 * gap)
{
int begin1 = j, end1 = j + gap - 1;
//int begin2 =j+gap, end2 = j+2*gap-1;
int begin2 = end1 + 1, end2 = begin2 + gap - 1;
int i = j;
// end1 begin2 end2 越界
// 修正区间 ->拷贝数据 归并完了整体拷贝 or 归并每组拷贝
if (end1 >= n)
{
end1 = n - 1;
// 不存在区间
begin2 = n;
end2 = n - 1;
}
else if (begin2 >= n)
{
// 不存在区间
begin2 = n;
end2 = n - 1;
}
else if (end2 >= n)
{
end2 = n - 1;
}
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
array[i++] = a[begin1++];
}
else
{
array[i++] = a[begin2++];
}
}
//防止前半部分或后半部分还剩下的有数据
while (begin1 <= end1)
{
array[i++] = a[begin1++];
}
while (begin2 <= end2)
{
array[i++] = a[begin2++];
}
}
// 也可以整体归并完了再拷贝
memcpy(a , array, sizeof(int) * n);
gap *= 2;
}
free(array);
array = NULL;
}
归并排序的特性总结:
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定