排序是一种将一组元素按照特定的顺序重新排列的操作。常见的排序方式包括升序(从小到大)、降序(从大到小)等。
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移。
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^2)
- 空间复杂度:
O(1)
- 稳定性:
稳定
先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。
1.普通插入排序的时间复杂度最坏情况下为O(N2),此时待排序列为逆序,或者说接近逆序。
2.普通插入排序的时间复杂度最好情况下为O(N), 此时待排序列为升序,或者说接近升序。
举个例子分析一下:
我们用序列长度的一半作为第一次排序时gap的值,此时相隔距离为5的元素被分为一组(共分了5组,每组有2个元素),然后分别对每一组进行直接插入排序。
gap的值折半,此时相隔距离为2的元素被分为一组(共分了2组,每组有5个元素),然后再分别对每一组进行直接插入排序。
gap的值再次减半,此时gap减为1,即整个序列被分为一组,进行一次直接插入排序。
前两趟就是希尔排序的预排序,最后一趟就是希尔排序的直接插入排序。
//希尔排序
void ShellSort(int* a, int n)
{
// 1、gap > 1 预排序
// 2、gap == 1 直接插入排序
int gap = n;
while (gap > 1)
{
gap = gap / 2 + 1;//gap折半并确保最后一趟gap = 1
int i = 0;
//进行一趟排序
for (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;
}
}
}
特点:
1. 希尔排序是对直接插入排序的优化。
2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。3. 希尔排序的时间复杂度不好计算,因为
gap的取值方法很多
,导致很难去计算。4. 稳定性:
不稳定
。
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
为了提高效率,我们可以一次把最大和最小的选出来.
- 在元素集合array[i]--array[n-1]中选择关键码最大(小)的数据元素
- 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
- 在剩余的array[i]--array[n-2](array[i+1]--array[n-1])集合中,重复上述步骤,直到集合剩余1个元素
//选择排序(一次选一个数)
void SelectSort(int* a, int n)
{
int i = 0;
for (i = 0; i < n; i++)//i代表参与该趟选择排序的第一个元素的下标
{
int start = i;
int min = start;//记录最小元素的下标
while (start < n)
{
if (a[start] < a[min])
min = start;//最小值的下标更新
start++;
}
Swap(&a[i], &a[min]);//最小值与参与该趟选择排序的第一个元素交换位置
}
}
直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:不稳定
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
有关建堆的思路可以见另一篇文章:堆的向下调整算法
建好堆后,如何排序呢?
1.将堆顶数据与堆的最后一个数据交换,然后对根位置进行一次堆的向下调整,但是调整时被交换到最后的那个最大的数不参与向下调整。
2.完成步骤1后,这棵树除最后一个数之外,其余数又成一个大堆,然后又将堆顶数据与堆的最后一个数据交换,这样一来,第二大的数就被放到了倒数第二个位置上,然后该数又不参与堆的向下调整…反复执行下去,直到堆中只有一个数据时便结束。此时该序列就是一个升序。
//向下调整建堆
void AdjustDwon(int* a, int size, int parent)
{
int child = parent * 2 + 1;
while (child < size)
{
if (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--)
{
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(N)
空间复杂度:O(1)
稳定性:不稳定
两个指针遍历数字,每次都把最大的给挪到最后要注意控制趟数
假如有n个元素,则走n - 1趟
每走一趟,俩指针需要走的趟数就少一次(n - 1 - i)
//冒泡排序
void BubbleSort(int* a, int n)
{
int end = 0;
for (end = n - 1; end >= 0; end--)
{
int exchange = 0;//记录该趟冒泡排序是否交换
int i = 0;
for (i = 0; i < end; i++)
{
if (a[i]>a[i + 1])
{
Swap(&a[i], &a[i + 1]);
exchange = 1;
}
}
if (exchange == 0)//该趟冒泡排序没有进行过交换,表面已经有序
break;
}
}
冒泡排序是一种非常容易理解的排序
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:稳定
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在应位置上为止。
选出一个key,一般是最左边或是最右边的。
经过一次单趟排序,最终使得key左边的数据全部都小于key,key右边的数据全部都大于key。
然后我们在将key的左序列和右序列再次进行这种单趟排序,如此反复操作下去,直到左右序列只有一个数据,或是左右序列不存在时,便停止操作,因为这种序列可以认为是有序的。
//快速排序(Hoare版本)
void QuickSort1(int* a, int begin, int end)
{
if (begin >= end)//当只有一个数据或是序列不存在时,不需要进行操作
return;
int left = begin;//L
int right = end;//R
int keyi = left;//key的下标
while (left < right)
{
//right先走,找小
while (left < right&&a[right] >= a[keyi])
{
right--;
}
//left后走,找大
while (left < right&&a[left] <= a[keyi])
{
left++;
}
if (left < right)//交换left和right的值
{
Swap(&a[left], &a[right]);
}
}
int meeti = left;//L和R的相遇点
Swap(&a[keyi], &a[meeti]);//交换key和相遇点的值
QuickSort1(a, begin, meeti - 1);//key的左序列进行此操作
QuickSort1(a, meeti + 1, end);//key的右序列进行此操作
}
将最左边的第一个值作为基准值,也作为坑位,从右边递减向左找到一个比基准值小的数,然后交换,并将该位置作为新的坑位,再从左边递增向右寻找比基准值大的数,然后交换,并将该位置作为新的坑位,依次进行,直到元素集合有序。
选出一个数据(一般是最左边或是最右边的)存放在key变量中,在该数据位置形成一个坑。
经过一次单趟排序,最终也使得key左边的数据全部都小于key,key右边的数据全部都大于key。
然后也是将key的左序列和右序列再次进行这种单趟排序,如此反复操作下去,直到左右序列只有一个数据,或是左右序列不存在时,便停止操作。
//快速排序(挖坑法)
void QuickSort2(int* a, int begin, int end)
{
if (begin >= end)//当只有一个数据或是序列不存在时,不需要进行操作
return;
int left = begin;//L
int right = end;//R
int key = a[left];//在最左边形成一个坑位
while (left < right)
{
//right向左,找小
while (left < right&&a[right] >= key)
{
right--;
}
//填坑
a[left] = a[right];
//left向右,找大
while (left < right&&a[left] <= key)
{
left++;
}
//填坑
a[right] = a[left];
}
int meeti = left;//L和R的相遇点
a[meeti] = key;//将key抛入坑位
QuickSort2(a, begin, meeti - 1);//key的左序列进行此操作
QuickSort2(a, meeti + 1, end);//key的右序列进行此操作
}
单趟排序
- 选出一个key,一般是最左边或是最右边的。
- 起始时,prev指针指向序列开头,cur指针指向prev+1。
- 若cur指向的内容小于key,则prev先向后移动一位,然后交换prev和cur指针指向的内容,然后cur指针++;若cur指向的内容大于key,则cur指针直接++。如此进行下去,直到cur指针越界,此时将key和prev指针指向的内容交换即可。
经过一次单趟排序,最终也能使得key左边的数据全部都小于key,key右边的数据全部都大于key。
然后也还是将key的左序列和右序列再次进行这种单趟排序,如此反复操作下去,直到左右序列只有一个数据,或是左右序列不存在时,便停止操作。
//快速排序(前后指针法)
void QuickSort3(int* a, int begin, int end)
{
if (begin >= end)//当只有一个数据或是序列不存在时,不需要进行操作
return;
//三数取中
int midIndex = GetMidIndex(a, begin, end);
Swap(&a[begin], &a[midIndex]);
int prev = begin;
int cur = begin + 1;
int keyi = begin;
while (cur <= end)//cur未越界
{
if (a[cur] < a[keyi] && ++prev != cur)//cur指向的内容小于key
{
Swap(&a[prev], &a[cur]);
}
cur++;
}
int meeti = prev;//cur越界时,prev的位置
Swap(&a[keyi], &a[meeti]);//交换key和prev指针指向的内容
QuickSort3(a, begin, meeti - 1);//key的左序列进行此操作
QuickSort3(a, meeti + 1, end);//key的右序列进行此操作
}
快速排序的时间复杂度是O(NlogN),是我们在理想情况下计算的结果。在理想情况下,我们每次进行完单趟排序后,key的左序列与右序列的长度都相同
但是,当待排序列本就是一个有序的序列时,我们若是依然每次都选取最左边或是最右边的数作为key,那么快速排序的效率将达到最低
这种情况下,快速排序的时间复杂度退化为O(N^2)。其实,对快速排序效率影响最大的就是选取的key,若选取的key越接近中间位置,则则效率越高。
//三数取中
int GetMidIndex(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 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 right;
else
return left;
}
}
三数取中,当中的三数指的是:最左边的数、最右边的数以及中间位置的数。三数取中就是取这三个数当中,值的大小居中的那个数作为该趟排序的key。这就确保了我们所选取的数不会是序列中的最大或是最小值了。
我们可以看到,就算是上面理想状态下的快速排序,也不能避免随着递归的深入,每一层的递归次数会以2倍的形式快速增长。
为了减少递归树的最后几层递归,我们可以设置一个判断语句,当序列的长度小于某个数的时候就不再进行快速排序,转而使用其他种类的排序。小区间优化若是使用得当的话,会在一定程度上加快快速排序的效率,而且待排序列的长度越长,该效果越明显。
//优化后的快速排序
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
return;
if (end - begin + 1 > 20)//可自行调整
{
//可调用快速排序的单趟排序三种中的任意一种
//int keyi = PartSort1(a, begin, end);
//int keyi = PartSort2(a, begin, end);
int keyi = PartSort3(a, begin, end);
QuickSort(a, begin, keyi - 1);//key的左序列进行此操作
QuickSort(a, keyi + 1, end);//key的右序列进行此操作
}
else
{
HeapSort(a + begin, end - begin + 1);
}
}
快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
时间复杂度:O(N*logN)
空间复杂度:O(logN)
稳定性:不稳定
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
//归并排序(子函数)
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;
int begin2 = mid + 1, end2 = right;
//将两段子区间进行归并,归并结果放在tmp中
int i = left;
while (begin1 <= end1&&begin2 <= end2)
{
//将较小的数据优先放入tmp
if (a[begin1] < a[begin2])
tmp[i++] = a[begin1++];
else
tmp[i++] = a[begin2++];
}
//当遍历完其中一个区间,将另一个区间剩余的数据直接放到tmp的后面
while (begin1 <= end1)
tmp[i++] = a[begin1++];
while (begin2 <= end2)
tmp[i++] = a[begin2++];
//归并完后,拷贝回原数组
int j = 0;
for (j = left; j <= right; j++)
a[j] = tmp[j];
}
//归并排序(主函数)
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);//申请一个与原数组大小相同的空间
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
_MergeSort(a, 0, n - 1, tmp);//归并排序
free(tmp);
}
归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
时间复杂度:O(N*logN)
空间复杂度:O(N)
稳定性:稳定
统计相同元素出现次数
根据统计的结果将序列回收到原来的序列中
//计数排序
void CountSort(int* a, int n)
{
//记录最小元素
int min = a[0];
//记录最大元素
int max = a[0];
for (int i = 0; i < n; i++)
{
if (min > a[i])
{
min = a[i];
}
if (max < a[i])
{
max = a[i];
}
}
int range = max - min + 1;
//不能用malloc,会让count数组有随机值
int* count = (int*)calloc(range, sizeof(int));
if (count == NULL)
{
printf("realloc fail\n");
exit(-1);
}
//统计次数
for (int i = 0; i < n; i++)
{
count[a[i] - min]++;
}
//返回原来数组a
int i = 0;
for (int j = 0; j < range; j++)
{
while (count[j]--)
{
a[i++] = j + min;
}
}
free(count);
}
从代码分析,三层循环都是O(N),一层是O(range),所以时间复杂度为O(N + range)。