目录
一、直接插入排序
基本思想
1.在前n个有序元素中插入一个数x。
2.从第n个数开始依次和x进行比较,比x大的数依次向后移一个单位,遇到比x小的元素,停止比较,插入x。
3.从数组的第一个元素开始,依次执行1和2直到整个数组有序。
实现
对于一个数组长度为n的整型数组,先考虑单趟的情况。
对于[0,end]的区间,将a[end+1]插入到合适的位置,区间内的元素为升序。
1.如果a[end+1]比至少一个区间内的元素大,则将其插入位置为比它小和比它大的元素中间;
2.如果a[end+1]比区间中的所有元素都小,则其插入位置为数组的首位。
代码:
void InsertSort(int* arr, int n)
{
int end = 0;
for (int i = 0;i < n - 1;i++)
{
end = i;
int x = arr[end + 1];//即将要插入前[0,end]区间元素的值
while (end >= 0)
{
//比x大的元素都往后挪一位
if (arr[end] > x)
{
arr[end + 1] = arr[end];
end--;
}
else
{
break;
}
}
arr[end + 1] = x;//插入
}
}
特点
1.数组元素越有序,直接插入排序的效率越高。
2.时间复杂度为O(N^2)。
当数组为逆序排列时,为最坏的情况,时间复杂度的精确值为1+2+3+...+(n-1)=n(n-1)/2
3.空间复杂度O(1)
二、希尔排序
基本思想
1.希尔排序于直接插入排序的区别是引入了间隔gap,对于一个数组而言,先进行预排序,再进行直接插入排序。
2.gap不断缩小的过程也就是预排序的过程。
3.gap=1则为直接插入排序。
实现
1.间隔gap大于1时的预排序本质也是插入排序。
代码:
void ShellSort(int* arr, int n)
{
int gap = n;
while(gap > 1)
{
//当gap>1时为预排序;gap=1时为直接插入排序
//预排序后数组已经接近有序,直接插入排序的效率高
gap = gap / 2;
for (int j = 0;j < n - gap;j++)
{
int end = j;
int x = arr[end + gap];
while (end >= 0)
{
if (arr[end] > x)
{
arr[end + gap] = arr[end];
end -= gap;
}
else
{
break;
}
}
arr[end + gap] = x;
}
}
}
特点
1.希尔排序是直接插入排序的变式
2.时间复杂度O(N^1.3)
3.空间复杂度O(1)
三、选择排序
基本思想
1.从数组中找到最大值或最小值;
2.将最大值或最小值与数组最右边或最左边的值进行交换。
3.对剩余数组元素(已经交换至最右边或最左边的元素不再参与)继续进行操作1和2。
实现
除了最基础的选择排序,还可进行优化,单趟排序查找数组的最大和最小元素,将其分别放到数组的最右端和最左端,然后对剩余数组做相同的操作,直到数组有序。
双指针法(双下标)。
代码:
void SelectSort(int* arr, int n)
{
int begin = 0;
int end = n - 1;
//保证剩余数组部分不为空
while (begin < end)
{
//设置数组最大值和最小值的初始下标为begin
int min = begin;
int max = begin;
//查找数组元素的最大值和最小值,并记录下标max和min
for (int i = begin + 1;i <= end;i++)
{
if (arr[i] < arr[min])
{
min = i;
}
if (arr[i] > arr[max])
{
max = i;
}
}
//将数组元素的最小值和数组首元素交换
Swap(&arr[min], &arr[begin]);
//由于数组首元素已经置换成最小元素,如果本来是数组最大元素,则需进行特殊处理,将最
大元素的下标置为最小元素的原下标
if (begin == max)
{
max = min;
}
//将数组元素的最大值和数组最后一个元素交换
Swap(&arr[max], &arr[end]);
//交换后原数组的最大元和最小元素不参与后续的排序
begin++;
end--;
}
}
特点
1.选择排序的工作量与数组长度直接挂钩,与数组的有序程度无关。
2.时间复杂度O(N^2)。
3.空间复杂度O(1),没有开辟额外的空间。
四、堆排序
基本思想
1.排升序,建大堆;排降序,建小堆。
(建大堆后,交换堆顶和最后一个元素,交换后不考虑最后一个元素,因为堆是用数组进行存储的,堆顶元素即数组的第一个元素,最后一个元素即数组的最后一个元素,所以排升序是建大堆,反之亦然。)
2.建堆结束后,开始进行排序的中心步骤——堆顶元素和最后一个元素进行交换,交换后的最后一个元素排除在外,堆顶元素进行向下调整。
3.继续执行步骤2,直到所有元素都被排除在外。
实现
1.建大堆可通过从最右下角的子树开始,从右向左,总下到上执行向下调整算法。
2.整个算法都是在原数组上进行操作,无需开辟额外空间。
代码:
//向下调整 建大堆
void AdjustDown(int* arr, int n, int parent)
{
assert(arr);
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n)
{
child = arr[child] > arr[child + 1] ? child : child + 1;
}
if (arr[parent] < arr[child])
{
Swap(&arr[parent], &arr[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//堆排序
void HeapSort(int* arr, int n)
{
//建大堆
for (int i = (n - 1 - 1) / 2;i >= 0;i--)
{
AdjustDown(arr, n, i);
}
//交换堆顶和堆中最后一个元素
//将end-1
//向下调整
int end = n - 1;
while (end > 0)
{
Swap(&arr[0], &arr[end]);
AdjustDown(arr, end, 0);
end--;
}
}
特点
1.数组元素的有序性对堆排序几乎没什么影响,建大堆本身会使数组变得无序。
2.时间复杂度O(NlogN)
3.空间复杂度O(1),没有开辟额外的空间。
五、冒泡排序
基本思想
1.从数组首元素开始,依次和右边的元素进行大小比较,如果比右边的元素大就进行值交换。
2.单趟排序至少能让最大的元素归位,最大的元素归位后不再参与后续的排序。
3.重复执行步骤1,最糟糕的情况需要有n-1趟排序。
4.设置变量来标志当趟排序是否有值交换,若没有则说明当趟排序时数组已经有序,无需再继续排序。
实现
1.利用循环实现多趟排序,设置flag来标志数组是否有序。
代码:
void BubbleSort(int* arr, int n)
{
for (int i = 0;i < n;i++)
{
int flag = 0;//假设数组已经有序
for (int j = 0;j < n - i - 1;j++)
{
//比较元素和右边相邻元素的大小,比右边元素大则进行值交换
if (arr[j] > arr[j + 1])
{
Swap(&arr[j], &arr[j + 1]);
flag = 1;
}
}
//已经有序了,无需继续进行排序
if (flag == 0)
break;
}
}
特点
1.由于设置了标志变量,数组越接近有序,排序的次数会越少。
2.但当数组元素较大时优化效果微乎其微。
3.时间复杂度:O(N^2)
4.空间复杂度O(1),没有开辟额外的空间。
六、快速排序
1.hoare版本
基本思想
1.将数组最左边元素的值赋值给key。
2.设置左右指针L和R,L和R错开移动,最左边元素作key,则R先动,以保证L和R相遇时所指的元素值小于key。
3.R指向的元素大于key时R向左移动,遇到小于key的元素时停止,然后L移动,遇到大于key的元素时停止,交换L和R所指元素。
4.再次执行步骤3,直到L和R相遇。
5.相遇后,交换相遇处的元素和最左边的元素。
6.递归实现整个数组的排序。
实现
1.用双指针,错开移动,相遇结束。
2.单趟排序结束后key归位,然后再分别对key左右两边的数组部分进行相同的操作,可用递归实现。
代码:
// 快速排序hoare版本
int PartSort1(int* arr, int begin, int end)
{
if (begin >= end)
return -1;
int left = begin, right = end;
int key_i = left;//左端点的值做key
while (left < right)
{
//right先走,找小
while (left < right && arr[right] >= arr[key_i])
{
--right;
}
//left再走,找大
while (left < right && arr[left <= arr[key_i]])
{
++left;
}
Swap(&arr[left], &arr[right]);
}
Swap(&arr[left], &arr[key_i]);
key_i = left;
return key_i;
}
void QuickSort(int* arr, int begin, int end)
{
if (begin >= end)
return;
int key_i = PartSort1(arr, begin, end);
//递归
QuickSort(arr, begin, key_i - 1);
QuickSort(arr, key_i + 1, end);
}
2.挖坑法
基本思想
1.挖坑版是hoare的“民间版“,挖坑的方法便于理解。
2.将数组首元素赋值给key,将首元素位置视为空,设置左右指针L和R,左右指针错开填坑。
3.右指针先动,遇到比key小的元素是停下来,将该较小元素填入首元素的位置,得到一个新的坑位;左指针开始移动,遇到比key大的元素停下来,将该较大元素填入新的坑位,直到L和R相遇,将key填入相遇时的坑位。
4.一轮操作后,key找准了自己的位置,而后对key左右的部分数组进行相同的操作,利用递归来实现。
实现
与hoare版本相似
代码:
// 快速排序挖坑法
int PartSort2(int* arr, int begin, int end)
{
if (begin >= end)
return -1;
int left = begin, right = end;
int key = arr[left];
int hole = left;//坑在左端
while (left < right)
{
//right先走,找小
while (left < right && arr[right] >= key)
{
--right;
}
//填左边的坑
arr[hole] = arr[right];
hole = right;//坑在右端
//left再走,找大
while (left < right && arr[left] <= key)
{
++left;
}
//填右边的坑
arr[hole] = arr[left];
hole = left;
}
arr[hole] = key;
return hole;
}
void QuickSort(int* arr, int begin, int end)
{
if (begin >= end)
return;
int key_i = PartSort2(arr, begin, end);
//递归
QuickSort(arr, begin, key_i - 1);
QuickSort(arr, key_i + 1, end);
}
3.前后指针法
基本思想
1.数组的首元素赋值给key。
2.设置前后指针prev和cur,将指针分别初始化为第一个元素和第二个元素的下标。
3.以cur为下标的元素与key比较,比key小则prev后移一位后与cur所指元素进行交换,否则cur向前挪动,prev不动。
4.执行3操作直到cur遍历整个数组,将prev所指元素和首元素进行交换,交换后key归位。
5.对key左右两边的数组进行相同的操作,利用递归实现。
实现
代码:
// 快速排序前后指针法
int PartSort3(int* arr, int begin, int end)
{
if (begin >= end)
return -1;
int prev = begin, cur = begin + 1;
int key_i = begin;
while (cur <= end)
{
//cur找比key小的值,找到后prev++,然后交换arr[prev]和arr[cur]
if(arr[cur] < arr[key_i])
{
Swap(&arr[++prev], &arr[cur]);
}
cur++;
}
Swap(&arr[prev], &arr[key_i]);
key_i = prev;
return key_i;
}
void QuickSort(int* arr, int begin, int end)
{
if (begin >= end)
return;
int key_i = PartSort3(arr, begin, end);
//递归
QuickSort(arr, begin, key_i - 1);
QuickSort(arr, key_i + 1, end);
}
4.非递归版本
基本思想
1.利用数据结构——栈,存储进行排序的起始和终止下标;
2.利用获取栈顶元素和出栈的方法以及栈“先进后出”的特点将key的左右区间的起止下标存入栈(先右后左);
3.获取栈顶元素、出栈相结合,得到待排序数组的起始和终止下标,调用三种排序函数中的一个,进行排序。
4.2和3相结合,直到栈为空,排序结束。
实现
代码:
// 快速排序 非递归实现
void QuickSortNonR(int* arr, int begin, int end)
{
Stack st;
StackInit(&st);
StackPush(&st, begin);
StackPush(&st, end);
while (!StackEmpty(&st))
{
int right = StackTop(&st);
StackPop(&st);
int left = StackTop(&st);
StackPop(&st);
int key_i = PartSort3(arr, left, right);
if (right > key_i+1)
{
StackPush(&st, key_i + 1);
StackPush(&st, right);
}
if (left < key_i-1)
{
StackPush(&st, left);
StackPush(&st, key_i - 1);
}
}
StackDestroy(&st);
}
5.总结
1.快速排序效率高,但递归调用栈帧消耗大,当数组长度较大时,可用通过小区间采用插入排序,以减少递归调用次数来减少调用栈帧的消耗。
2.当被排序数组几乎接近有序时,快排处于劣势,时间复杂度为O(N^2),可采用三数取中的方法来进行优化。但是对于几乎全部相同的数组这种方法不能解决。
代码:
//三数取中
int GetMidIndex(int* arr, int begin, int end)
{
int mid = (begin + end) / 2;
if (arr[begin] < arr[end])
{
if (arr[begin] > arr[mid])
return begin;
else
{
if (arr[mid] < arr[end])
return mid;
else
return end;
}
}
else//arr[begin] > arr[end]
{
if (arr[end] > arr[mid])
return end;
else
{
if (arr[begin] > arr[mid])
return mid;
else
return begin;
}
}
}
3.时间复杂度O(NlogN)
4.空间复杂度O(logN) 递归建立函数栈帧
七、归并排序
基本思想
1.归并排序的基本思想时分治思想,即分而治之。
2.将数组进行区间的划分,将一个个区间进行排序,而单个元素可以被视为一个个有序的区间,所以”分“的最底层结果是将数组拆分成一个个元素。
3.将有序的区间进行合并,再重新排序使之有序。排序的方法是在一个额外的数组空间里进行”尾插“,即将两个有序区间的较小值进行插入,直到两个区间的值都插入完毕,得到的即为两个有序区间合并后的有序区间。
4.用递归的思想或非递归的思想来实现区间的”分“和”并“。
实现
1.递归版本
与二叉树的前序遍历相似。
代码:
void _MergeSort(int* a, int begin, int end, int* tmp)
{
//理论上不可能出现begin>end的情况,但是当begin=end时,
//默认该区间为有序,不用进行归并,这是递归的出口
if (begin >= end)
return;
int mid = (begin + end) / 2;
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
//递归
_MergeSort(a, begin1, end1, tmp);
_MergeSort(a, begin2, end2, tmp);
//归并
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 failed!");
}
_MergeSort(a, 0, n - 1, tmp);
//释放动态申请的内存
free(tmp);
tmp = NULL;
}
2.非递归版本
递归版本调用堆栈较多,为了减少额外空间的消耗,可以利用循环的方法来控制区间大小,为非递归方法实现归并排序的关键。与递归不同的是,非递归的单趟循环会使整个数组的相同小区间都变得有序,区间大小的两倍大于等于数组后,整个数组都有序。
代码:
// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
// 归并每组数据个数,从1开始,因为1个认为是有序的,可以直接归并
int rangeN = 1;
while (rangeN < n)
{
for (int i = 0; i < n; i += 2 * rangeN)
{
// [begin1,end1][begin2,end2] 归并
int begin1 = i, end1 = i + rangeN - 1;
int begin2 = i + rangeN, end2 = i + 2 * rangeN - 1;
//控制临时数组的下标
int j = i;
// 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])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
}
// 也可以整体归并完了再拷贝
memcpy(a, tmp, sizeof(int) * n);
rangeN *= 2;
}
free(tmp);
tmp = NULL;
}
特点
1.需要借助额外的数组,空间复杂度为O(N)
2.时间复杂度O(NlogN),数组的有序性不影响算法的效率。
3.分而治之思想的应用。
八、计数排序
基本思想
1.找到数组a的最大元素和最小元素,计算range = a[max]-a[min]+1,动态申请额外的数组空间tmp,长度为range,并初始化数组元素为0;
2.对以a[i]-min为下标的tmp元素+1,从而达到计数的效果。
3.遍历整个数组a,执行操作2,然后再将数组tmp中的元素下标+min拷贝回原数组,拷贝一个元素-1,值为0时拷贝下一个,直到tmp数组全为0,得到的拷贝数组即为排序后的数组。
实现
代码:
// 计数排序
void CountSort(int* a, int n)
{
int min = a[0], max = a[0];
for (int i = 0;i < n;i++)
{
if (a[i] < min)
{
min = a[i];
}
if (a[i] > max)
{
max = a[i];
}
}
int range = max - min + 1;
int* tmp = (int*)calloc(range, sizeof(int));
if (tmp == NULL)
{
perror("calloc fail");
exit(-1);
}
//1.计数
for (int i = 0;i < n;i++)
{
tmp[a[i] - min]++;
}
//2.排序
int i = 0;
for (int j = 0;j < range;j++)
{
while(tmp[j] > 0)
{
a[i++] = j + min;
tmp[j]--;
}
}
free(tmp);
tmp = NULL;
}
特点
1.非比较排序,不是通过数组元素之间进行比较来进行排序;
2.有局限性,适用于极差小的数组。数组元素越集中,算法的效率越高。
3.只适合整型,不能用于浮点数的排序。
4.时间复杂度O(N+range)
5.空间复杂度O(range)
九、排序算法的稳定性
排序算法的稳定性是指,对于未排序前的数组,排序后相同值的元素的相对位置有没有可能会发生改变;排序后,相同元素的相对位置不发生改变,则该排序算法是稳定的,反之亦然。
故,上述八大排序中,稳定排序有:直接插入排序、冒泡排序、归并排序、计数排序;
不稳定排序有:希尔排序、选择排序、堆排序、快速排序。
(只有在排序前相同元素的顺序有意义时,稳定性才有意义。)