目录
[3. 选择排序](#3. 选择排序)
[6. 快速排序](#6. 快速排序)
[6.3 前后指针法](#6.3 前后指针法)
1.插入排序
基本思想:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
end指得是要插入元素下标的前一位,tmp指得是要插入的元素,当tmp<a[end]时,a[end]位置的值往后移动一位,依次循坏,直到tmp>=a[end]的值,在a[end+1]位置放入tmp的值
代码如下:
void InsertSort(int* a, int n)
{
for (int i = 1; i < n; i++)
{
int end = i - 1;
int tmp = a[i];
while (end >= 0)
{
if (tmp < a[end])
{
a[end+1] = a[end];
--end;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
直接插入排序的特性总结:
元素集合越接近有序,直接插入排序算法的时间效率越高
时间复杂度:最坏情况下为O(N*N),此时待排序列为逆序,或者说接近逆序
最好情况下为O(N),此时待排序列为升序,或者说接近升序。空间复杂度:O(1),它是一种稳定的排序算法
稳定性:稳定
2.希尔排序
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。
简单来说,就是分组进行排序,把数组里的元素分成不同的组进行排序(这种排序的内核就是插入排序),刚开始时gap(gap指的是组中元素的间距)比较大,让数组更加有序,最后当gap=1时(此时就是插入),数组已经接近有序,在进行插入排序可以节省大量的时间,时间复杂度大大降低
代码如下:
// 希尔排序
void ShellSort(int* a, int n)
{
int gap = n;
int i = 0;
while (gap > 0)
{
gap /= 2;
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;
}
}
}
希尔排序的特性总结:
希尔排序是对直接插入排序的优化。
当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定
时间复杂度平均:O(N^1.3)
空间复杂度:O(1)稳定性:不稳定
3. 选择排序
基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
思路:
在数组中选出最大的值和最小的值,将其分别放在数组的尾和头,以此循坏
代码如下:
void Swap(int* p1, int* p2)
{
int x = *p1;
*p1 = *p2;
*p2 = x;
}
void SelectSort(int* a, int n)
{
int left = 0;
int right = n - 1;
int i = 0;
while (left < right)
{
//min指最小值的下标 , max指最大值下标
int min = left, max = left;
for (i = left ; i <= right; i++)
{
if (a[i] < a[min])
{
min = i;
}
if (a[i] > a[max])
{
max = i;
}
}
//将最大值放到right ,最小值放到left
Swap(&a[left], &a[min]);
//当left 和 max 指的是同一个位置,此时原本left的值与min交换,所以要将min赋值给max;
if (left == max)
{
max = min;
}
Swap(&a[right], &a[max]);
left++;
right--;
}
}
直接选择排序的特性总结:
直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:不稳定
4.冒泡排序
基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
代码如下 :
void Swap(int* p1, int* p2)
{
int x = *p1;
*p1 = *p2;
*p2 = x;
}
void BubbleSort(int* a, int n)
{
int i = 0;
int j = 0;
for (j = 0; j < n-1; j++)
{
int flag = 1;
for (i = 0; i < n - 1 - j; i++)
{
if (a[i] > a[i + 1])
{
Swap(&a[i], &a[i + 1]);
flag = 0;
}
}
if(flag == 1)
{
break;
}
}
}
冒泡排序的特性总结:
冒泡排序是一种非常容易理解的排序
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:稳定
5.堆排序
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
向下调整算法的核心思想:选出左右孩子中小的哪一个,跟父亲交换,小的往上浮,大的往下沉,如果要建大堆则相反
首先需要建立一个大堆,然后将大堆中的第一个元素与最后一元素交换,再进行向下调整,将堆重新调整为大堆,便可的到升序数组
代码如下:
//升序
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//建立在堆是大堆的情况
void AdjustDown(int* a, int n, int parent)
{
assert(a);
int child = parent * 2 + 1;
while (child < n)
{
//判断孩子大小
if (child + 1 < n && a[child] < a[child + 1])
{
child++;
}
//当父母小于孩子时,交换
if (a[parent] < a[child])
{
Swap(&a[parent], &a[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int* a, int n)
{
int i = 0;
//大堆
//向下调整 复杂度O(N)
for (i = ((n - 2) / 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--;
}
}
堆排序的特性总结:
堆排序使用堆来选数,效率就高了很多。
时间复杂度:O(N*logN)
空间复杂度:O(1)
稳定性:不稳定
6. 快速排序
基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
6.1hoare版本
思路:
1.选出一个keyi(数组下标),数组最左边或者最右边(这里我默认为最左边)
2.定义一个left和right分别指的是数组第一个元素的下标和数组最后一个元素的下标,left从左往右走,right从右往左走
3.(如果keyi是最左边,则right先走,若keyi为最右边,则相反)right先从右往左走找出 比keyi所在位置元素要小的元素停下,然后left开始从左往右走,找出比keyi所在位置元素要大的元素停下,然后将left和right所在元素进行交换,然后right继续走,直到lright最终相遇,此时将相遇点的内容与keyi交换
4.这是keyi位置的左边都是小于等于keyi内容的,而右边都是大于等于keyi内容
5.将keyi左边序列和右边序列,重复上述操作,直到左右序列只有一个数据,或是不存在时,便停止操作
代码如下:
void Swap(int* p1, int* p2)
{
int x = *p1;
*p1 = *p2;
*p2 = x;
}
int PartSort1(int* a, int left, int right)
{
int keyi = left; //可以使用三数求中提高效率
while (left < right)
{
//找小
while (left < right && a[right] >= a[keyi])
{
--right;
}
//找大
while (left < right && a[left] <= a[keyi])
{
left++;
}
Swap(&a[left], &a[right]);
}
//left 和 right 相遇
Swap(&a[keyi], &a[left]);
keyi = left;
return keyi;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int keyi = PartSort1(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
6.2挖坑法
1.选出一个key(最左边或最右),key指的是数组的元素(不是下标!),key会被保存下来
2.key位置(下标)即为hole(坑),类似hoare的方法,right开始先走(找比key小)停下之后,将right的内容放到hole位置中(即为填坑),此时right位置成为了坑,之后left开始走(找比key大),
将left的内容放到hole位置中,以此反复,当left和right相遇时,将key放到坑中
3.将key左边序列和右边序列,重复上述操作,直到左右序列只有一个数据,或是不存在时,便停止操作
代码如下:
void Swap(int* p1, int* p2)
{
int x = *p1;
*p1 = *p2;
*p2 = x;
}
// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
int key = a[left];
int begin = left, end = right;
int hole = left;
while (left < right)
{
while (left < right && a[right] >= key)
{
right--;
}
a[hole] = a[right];
hole = right;
while (left < right && a[left] <= key)
{
left++;
}
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int keyi = PartSort2(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
6.3 前后指针法
1.将key左边序列和右边序列,重复上述操作,直到左右序列只有一个数据,或是不存在时,便停止操作
代码如下:
1.选出一个keyi(数组下标),数组最左边或者最右边(这里我默认为最左边)
2.定义一个prev和cur ,prev指的是最左边(下标),而cur指的是prev后一位
3.cur开始从左往右走
3.1 当cur遇到比keyi内容要小的值时,prev往后移动一位,并将cur的内容与prev内容交换,最后cur往后移动一为
3.2 若cur遇到比keyi内容要大的值时,cur往后一位
4.当cur走出数组,将prev内容与keyi内容交换
5.最后
void Swap(int* p1, int* p2)
{
int x = *p1;
*p1 = *p2;
*p2 = x;
}
// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
int keyi = left;
int prev = left;
int cur = left + 1;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
Swap(&a[cur], &a[prev]);
}
++cur;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int keyi = PartSort3(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
6.4非递归法
思路:
1.非递归就是通过建立一个栈,将原本递归的范围放到栈中,不断出栈和入栈,最后栈为空时结束循环
2.刚开始时要先将 right,left压入栈中,然后取出来之后,将其用前后指针法排序(也可以是其他方法)得到关键值,然后再将右序列范围的right和left压入栈中,左序列范围的right和left压入栈中,重复以上操作,当栈为空退出
// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
ST st;
StackInit(&st,20);
StackPush(&st, right);
StackPush(&st, left);
while (StackEmpty(&st))
{
int begin = StackTop(&st);
StackPop(&st);
int end = StackTop(&st);
StackPop(&st);
//前后指针
int keyi = PartSort3(a, begin, end);
//left,keyi-i /keyi+1,end
//当数据大于等于2
if (keyi + 1 < end)
{
StackPush(&st, end);
StackPush(&st, keyi + 1);
}
if (begin < keyi - 1)
{
StackPush(&st, keyi - 1);
StackPush(&st, begin);
}
}
}
6.5快速排序优化
三数取中法选keyi
nt Midkeyi(int* a, int left, int right)
{
int mid = (left + right) / 2;
if (a[left] < a[mid])
{
if (a[left] > a[right])
{
return left;
}
else if (a[mid] < a[right])
{
return mid;
}
else
{
return right;
}
}
else // left > mid
{
if (a[left] < a[right])
{
return left;
}
else if (a[mid] > a[right])
{
return mid;
}
else
{
return right;
}
}
}
快速排序的特性总结:
快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
时间复杂度:O(N*logN)
空间复杂度:O(logN)
稳定性:不稳定
7.归并排序
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
7.1递归
简单来说:
1.先将数组分开,即一分二,二分四....,分到只有一个元素的序列或者不存在
2.将序列进行比较,小的先放入到创建的临时数组tmp中,大的再依次放入,合成一个有序序列
3.将子序列两两合并,每合并一次,就会产生一个新的且更长的有序表,重复上述步骤,直到最后只剩下一个序列, 即....四成二,二成一。
代码如下:
// 归并排序递归实现
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin >= end)
{
return;
}
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[begin2++];
}
else
{
tmp[i++] = a[begin1++];
}
}
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::");
return;
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
}
7.2非递归
思路:
递归是将数组分成最小个进行比较合并,而我们只需要一个循坏即可
1.定义一个变量gap,gap指的是相邻几个数之间比较,gap以2的倍数增长,
当gap = 1时,即每两个数之间进行了比较交换,
当gap = 2 时,即每四个数之间进行了比较交换,
........
当gap = n/2时,即没n个数之间进行了比较交换,
2.需要注意的是,这种方法可能会造成数组的越界访问,所以要有越界的处理
代码如下:
void _MergeSortNonR(int* a, int n, int* tmp)
{
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
int j = i;
//越界处理
if (begin2 >= n)
{
break;
}
if (end2 >= n)
{
end2 = n - 1;
}
//
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] >= a[begin2])
{
tmp[j++] = a[begin2++];
}
else
{
tmp[j++] = a[begin1++];
}
}
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
}
gap *= 2;
}
}
// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc::");
return;
}
_MergeSortNonR(a, n, tmp);
free(tmp);
}
归并排序的特性总结:
归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
时间复杂度:O(N*logN)
空间复杂度:O(N)
稳定性:稳定