目录
一、常见的排序
二、冒泡排序
2.1基本思想:
2.2代码:
三、插入排序
3.1基本思想:
3.2思路讲解:
3.3代码:
3.4时间复杂度:
四、希尔排序
4.1基本思路:
4.2思路讲解:
4.3代码:
4.4时间复杂度:
五、选择排序
5.1基本思路:
5.2思路讲解:
5.3代码:
5.4时间复杂度:
六、堆排序
6.1基本思路:
6.2思路讲解:
6.3代码:
6.4时间复杂度:
七、快速排序(hoare版本)
7.1基本思路:
7.2思路讲解:
7.3代码:
7.4时间复杂度:
八、快速排序(挖坑法和前后指针版本)
8.1挖坑法
8.1.1挖坑法讲解
8.1.2代码:
8.2前后指针版本
8.1.1前后指针讲解
8.1.2代码:
九、归并排序
9.1基本思路:
9.2思路讲解:
9.3代码:
9.4时间复杂度:
一、常见的排序
二、冒泡排序
2.1基本思想:
冒泡排序,何为冒泡,即把数据最大的每次都排到最后一个位置,像冒泡一样。
2.2代码:
void BubbleSort(int* a, int n)
{
int i = 0;
for (; i < n; i++)
{
int j = 1;
for (; j < n - i; j++)
{
if (a[i - 1] > a[i])
Swap(&a[i - 1], &a[i]);
}
}
}
三、插入排序
3.1基本思想:
把待排序的数据逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
3.2思路讲解:
以上图数据、升序为例。
为了便于理解,我们分为两大阵营,已排和未排。我们在没排序之前,这些数据全都是未排阵营,接着我们开始比较,由于已排阵营没有数据,所以9就是第一个。然后拿未排阵营的33和9比较,33放在9之后。
接着就是10和33比较,10放在33前;10和9排序,10放在9后,此趟结束。
46和33比较,46放在33后,此趟结束。
23和46比较,放在46前;23和33比较,放在33前;23和10比较,放在10后,此趟结束。
我相信说了这几趟,大家就能基本明白,插入排序就是把没排序的和已排序的从后向前依次比较然后找到不满足条件的位置把自己放进去。
3.3代码:
首先,我们先完成单趟排序。
然后,我们介绍一下需要新引入的变量:
其中,因为我们需要把数组看成两部分,所以我们引入end作为两者的分界点;当挪动数据时,我们用前一个数据覆盖后一个数据,所以需要tmp记录被覆盖元素的值,即当前插入的元素。
当我们的插入元素不满足条件时,即它在前后两个元素之间,此时end移动到了前一个元素,所以后来我们需要把end+1赋值为插入的数值:
void InsertSort(int arr[], int n)
{
int end = 0;
int tmp = arr[end + 1];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + 1] = arr[end];
end--;
}
else
{
break;
}
}
arr[end + 1] = tmp;
}
我们把end作为了已排数列的最后一个元素的位置,由此可知我们的整套排序,end从0到n-1:
void InsertSort(int arr[], int n)
{
int i = 0;
for (; i < n - 1; i++)
{
int end = i;
int tmp = arr[end + 1];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + 1] = arr[end];
end--;
}
else
{
break;
}
}
arr[end + 1] = tmp;
}
}
3.4特点:
时间复杂度:
最坏情况:O(N^2) —— 逆序
最坏情况:O(N) —— 顺序有序
空间复杂度:
O(1)
稳定性:
稳定
什么是稳定性?
保证排序前相同的两个数的相对位置在排序后仍保持不变。
四、希尔排序
4.1基本思路:
1.预排序:取间隔的数,为一组,对每一组进行插入排序
2.插入排序:对整体进行插入排序
4.2思路讲解:
4.3代码:
我们把插入排序的单趟间隔从1改为gap,完成希尔排序的第一步:
void ShellSort(int arr[], int n)
{
int end = 0;
int gap = 3;
int tmp = arr[end + 1];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + gap] = arr[end];
end -= gap;
}
else
{
break;
}
}
arr[end + gap] = tmp;
}
把以上操作重复gap次即为所求代码:
void ShellSort(int arr[], int n)
{
int gap = 3;
for (int j = 0; j < gap; j++)
{
for (int i = j; i < n - gap; i += gap)
{
int end = i;
int tmp = arr[end + 1];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + gap] = arr[end];
end -= gap;
}
else
{
break;
}
}
arr[end + gap] = tmp;
}
}
}
我们可以对上述代码进行优化,让它变为gap组同时排序:
void ShellSort(int arr[], int n)
{
int gap = 3;
for (int i = 0; i < n - gap; i++)
{
int end = i;
int tmp = arr[end + gap];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + gap] = arr[end];
end -= gap;
}
else
{
break;
}
}
arr[end + gap] = tmp;
}
}
我们这还只是完成了第一步,接下来我们要进行插入排序:
void ShellSort(int arr[], int n)
{
int gap = 3;
for (int i = 0; i < n - gap; i++)
{
int end = i;
int tmp = arr[end + gap];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + gap] = arr[end];
end -= gap;
}
else
{
break;
}
}
arr[end + gap] = tmp;
}
int i = 0;
for (; i < n - 1; i++)
{
int end = i;
int tmp = arr[end + 1];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + 1] = arr[end];
end--;
}
else
{
break;
}
}
arr[end + 1] = tmp;
}
}
学到这里,其实这些分布的并非是真正的希尔排序,gap到底取多少的值?真的要分成两步?其实希尔排序的gap是随时变换的,最后gap的值为1时,才正好进行最后的插入排序,但gap的取值官方也未说明何时最好,我们只需要保证最后一次排序gap的值刚好为1即可:
void ShellSort(int arr[], int n)
{
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;
for (int i = 0; i < n - gap; i++)
{
int end = i;
int tmp = arr[end + gap];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + gap] = arr[end];
end -= gap;
}
else
{
break;
}
}
arr[end + gap] = tmp;
}
}
}
4.4特点:
希尔排序的复杂度分析是个复杂的问题,它的计算还涉及到数学领域中尚未解决的难题,但有人指出当n在特定范围时,其复杂度接近O(n^1.3),当n->无穷大时,可以减少到O(n*(logn)^2)
时间复杂度:O(n^1.3)
空间复杂度:O(1)
稳定性:不稳定
五、选择排序
5.1基本思路:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
5.2思路讲解:
选择排序其实没有什么难点,就是挨个比较然后将最小的换到首位,但其实我们可以同时找最大和最小值,然后让其找到对应的位置后再继续找次大的和次小的。
但是有一点需要注意,那就是当最后我们交换时,如果begin位置和maxi位置重合,那么先交换mini和begin的话,就无法找到正确的maxi值,反之同理,所以我们要多加一层判断来确保。
5.3代码:
首先我们先看一下我们需要新添加的变量:
1.标记交换最小值位置的begin和交换最大值位置的end,当一次选择完成后,他们标记的位置就会向后和向前挪动1个单位。
2.标记最大值最小值位置的maxi和mini,我们要记录当前最大值与最小值的元素下标,当每趟结束后与先前记录的begin和end交换。
void SelectSort(int arr[], int n)
{
int begin = 0;
int end = n - 1;
while (begin < end)
{
int mini = begin;
int maxi = begin;
for (int i = begin; i <= end; i++)
{
if (arr[i] < arr[mini])
{
mini = i;
}
if (arr[i] > arr[maxi])
{
maxi = i;
}
}
Swap(&arr[mini], &arr[begin]);
if (maxi == begin)
{
maxi = mini;
}
Swap(&arr[maxi], &arr[end]);
begin++;
end--;
}
}
5.4特点:
时间复杂度:
最好的情况:O(n^2)
最坏的情况:O(n^2)
空间复杂度:O(1)
稳定性:不稳定
六、堆排序
6.1基本思路:
利用大堆或小堆,每次找到一个最大值或最小值作为根结点并输出,再次进行堆排序,找到次大或次小值,重复上述操作。
6.2思路讲解:
6.3代码:
void AdjustUp(int* a, int child)
{
while (child > 0)
{
int parent = (child - 1) / 2;
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
}
else
{
return;
}
}
}
void AdjustDown(int* a, int size, int parent)
{
//假设左孩子小
int child = parent * 2 + 1;
while (child < size)
{
if (child + 1 < size && a[child] < a[child + 1])
{
child = child + 1;
}
if (a[parent] < a[child])
{
Swap(&a[parent], &a[child]);
parent = child;
child = child * 2 + 1;
}
else
{
return;
}
}
}
//升序
void HeapSort(int* a, int n)
{
//建大堆
for (int i = 1; i < n; i++)
{
AdjustUp(a, i);
}
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}
6.4特点:
时间复杂度:O(N*logN)
空间复杂度:O(1)
稳定性:不稳定
七、快速排序(hoare版本)
7.1基本思路:
任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
7.2思路讲解:
我们需要用两个指针,一个从前走,一个从后走,依次筛选满足条件的值,若不满足条件则交换。
还有一个小细节:我们要让右边指针先走,这样左右指针相遇时永远指向比key小的值,这样让left和key交换才能完成排序。
下面我们先来完成单趟,选用一个key值作为基准值,小的排在左边,大的排在右边,这样一次可以把整个数组分为两部分,然后我们还要进行基准值位置的调整,此时的left位置就是key应在的位置。
然后我们再分别对左右两边进行同样的步骤,即递归,当只剩下一个元素时就可以结束我们的递归操作,此时我们的数组也全部排序完成。
7.3代码:
首先我们来看新加变量,left和right指针,负责分别从两侧检索,keyi为此次所选的基准值的下标,但是需要注意的是,我们的left指针和keyi指针不能指向一个位置,我们的keyi指向的元素是不能和right指向的值进行交换的!
void QuickSort(int* a, int n)
{
int right = n - 1;
int keyi = 0;
int left = keyi + 1;
while (left < right)
{
//右边找小
while (left < right && a[right] >= a[keyi])
{
right--;
}
//左边找大
while (left < right && a[left] <= a[keyi])
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
}
完成了单趟排序以后,我们要进行递归调用,所以此时我们函数体引用不便再使用n作为区间标记,而是需要改用begin和end作为区间的分割点,使我们调用函数更加准确,然后递归调用的区间就是[begin,keyi-1] key [key+1,end]
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
return;
int keyi = begin;
int right = end;
int left = begin;//left和begin位置需要一样 坑一
while (left < right)
{
//右边找小
while (left < right && a[right] >= a[keyi])//前面条件 坑二 后面条件=号 坑三
{
right--;
}
//左边找大
while (left < right && a[left] <= a[keyi])
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
keyi = left;
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
7.4特点:
时间复杂度:O(N*logN)
空间复杂度:O(N)
稳定性:不稳定
八、快速排序(挖坑法和前后指针版本)
8.1挖坑法
8.1.1挖坑法讲解
8.1.2代码:
我们先把QuickSort的代码改一改,让它能更方便的测试我们的三个方法:
//hoare版本
int PartSort1(int* a, int begin, int end)
{
int keyi = begin;
int right = end;
int left = begin;
while (left < right)
{
//右边找小
while (left < right && a[right] >= a[keyi])
{
right--;
}
//左边找大
while (left < right && a[left] <= a[keyi])
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
return left;
}
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
return;
int keyi = PartSort1(a, begin, end);
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
我们再来看挖坑法:我们使用数组额外的新变量key来直接控制基准值,我认为这比用数组的某个下标控制更加稳定,然后每次都把坑用key的值填上也是为了更加稳定的控制:
//挖坑法
int PartSort2(int* a, int begin, int end)
{
int hole = begin;
int key = a[begin];
while (begin < end)
{
if (begin != hole)
{
while (begin < end && a[begin] <= key)
{
begin++;
}
a[end] = a[begin];
a[begin] = key;
hole = begin;
}
if (end != hole)
{
while (begin < end && a[end] >= key)
{
end--;
}
a[begin] = a[end];
a[end] = key;
hole = end;
}
}
return hole;
}
8.2前后指针版本
8.1.1前后指针讲解
其实画图把整个流程完整的走一遍我们就可以知道我们的目的是为了让prev和cur指针中间的值都是比key大的值,然后prev++负责记录下一个大的值,cur负责记录比key小的值,每次找到就交换,最后再把prev的值变为key即可。
如果看不懂,建议自己画一遍实操一下即可。
8.1.2代码:
//前后指针版本
int PartSort3(int* a, int begin, int end)
{
int key = a[begin];
int prev = begin;
int cur = begin + 1;
while (cur <= end)
{
if (a[cur] < key)
{
prev++;
Swap(&a[cur], &a[prev]);
}
cur++;
}
Swap(&a[prev], &a[begin]);
return prev;
}
我们也可以在第一个大while循环中把if换成while:
int PartSort3(int* a, int begin, int end)
{
int key = a[begin];
int prev = begin;
int cur = begin + 1;
while (cur <= end)
{
while (cur <= end && a[cur] > key)
{
cur++;
}
if (cur <= end)
{
prev++;
Swap(&a[prev], &a[cur]);
cur++;
}
}
Swap(&a[prev], &a[begin]);
return prev;
}
九、归并排序
9.1基本思路:
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;
即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
9.2思路讲解:
我们的归并排序是运用分治法的一个排序,下面是分的过程:
我们看到了控制分停止的条件就是begin>=end,下面我们看合的过程:
每次合都会把合到一起的几个元素排序,因为我们是数组, 所以我们还需要一个新数组负责挪动数据。
9.3代码:
根据思路讲解,我们先来看分的过程:
if (begin >= end)
return;
//分
int mid = (begin + end) / 2;
MergeSort(a, begin, mid, tmp);
MergeSort(a, mid + 1, end, tmp);
然后我们只需要完成一次合的过程:
//合
int begin1 = begin;
int end1 = mid;
int begin2 = mid + 1;
int end2 = end;
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++];
}
最后把tmp复制到我们正式的数组上即可:
memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
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;
int end1 = mid;
int begin2 = mid + 1;
int end2 = end;
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));
}
9.4特点:
时间复杂度:O(N*logN)每层的时间复杂度是O(N),因为二分,一共有logN层。
空间复杂度:O(N)
稳定性: