选择排序
选择排序是寻找数组中的最大值(最小值)的下标,然后交换。
void SelectSort(vector<int>& nums) {
int mins = 0;
for (int i = 0; i < nums.size() - 1; i++) {
mins = i;
for (int j = i; j < nums.size(); j++) {
if (nums[j] < nums[mins]) {
mins = j;
}
}
swap(nums[i], nums[mins]);
}
}
当然我们可以优化一下,上面原始做法是找最大值,其实我们可以分区间去找最小和最大,然后把最小值和该区间第一个数交换,最大值和该区间最后一个交换。
但是这样写会存在一个问题,就是当maxi的位置和begin的位置相同,mini的问题和end的位置相同,那么就会重复交换,相当于两次交换的同样的位置,那么没有起到交换的作用。
void SelectSort(vector<int>& nums) {
int mini, maxi;
int begin = 0;
int end = nums.size() - 1;
while (begin < end) { // 区间为[begin, end]
mini = maxi = begin;
for (int i = begin + 1; i <= end; i++) {
if (nums[i] < nums[mini]) {
mini = i;
}
if (nums[i] > nums[maxi]) {
maxi = i;
}
}
swap(nums[mini], nums[begin]);
// 如果maxi于begin位置重合,那么maxi的位置需要修正
if(maxi == begin){
maxi = mini;
}
swap(nums[maxi], nums[end]);
++begin;
--end;
}
}
选择排序其实效果很差,比插入还差。当数组有序的时候。
冒泡排序
void BubleSort(vector<int>& nums) {
for(int i = 0; i < nums.size() - 1; i++){
for(int j = 0; j < nums.size() - 1; j++){
if(nums[j] > nums[j+1])
swap(nums[j], nums[j+1]);
}
}
}
插入排序
时间复杂度:O(n^2)
void InsertSort(vector<int>& nums) {
for (int i = 0; i < nums.size() - 1; i++) {
int end = i;
int temp = nums[end+1];
while (end >= 0) {
if (temp >= nums[end]) { // 当已经大于,满足升序条件,不需要排
break;
}
else { //当小于,不断往后移动元素,也就是找插入位置
nums[end + 1] = nums[end];
end--;
}
}
nums[end + 1] = temp; // 找到了位置,就插入
}
}
希尔排序
希尔排序就是在插入排序上进行优化,每一次的排序,都是把数组变得更加有序。把数组分成等差的,定义gap,将数组分成两部分,假如是升序排序,那么每一次比较前面部分的和后部分,假如前大于后,那么就进行插入,如果小于,就原地插入。假如gap 等于1,那么就已经排好了。
与插入排序的比较:插入排序是把元素一个一个向后移,希尔排序是把元素向后移动gap个。
时间复杂度:O(n^1.3 ~ n^2)
void ShellSort(int* arr, int size)
{
int gap = size;
while (gap > 1)
{
gap = gap / 3 + 1; //调整希尔增量, 加1保证最后一次gap等与1
for (int i = 0; i < size - gap; i++) //从0遍历到size-gap-1
{
int end = i;
int temp = arr[end + gap];
while (end >= 0)
{
if (arr[end] > temp)
{
arr[end + gap] = arr[end];
end -= gap;
}
else
{
break;
}
}
arr[end + gap] = temp; //以 end+gap 作为插入位置
}
}
}
计数排序
计数排序本质上就是一个哈希,原始的做法就是开一个数组中最大值大小的数组,然后遍历待排序数组,通过映射(开辟的数组的下标等于待排序数组的元素,每遍历一次待排序数组的元素,就在开辟的数组对应映射的下标添加一次次数)。但是可以在这个基础上进行优化,缩小开辟数组的空间,假如按照原始方法去开辟数组大小,假如原始数组的最大值过大,那么会开辟一个空间很大的数组,那么就会造成空间浪费,空间复杂度很高。那么优化的思路就是降低这个开辟数组的大小,方法就是取待排序数组的最大值和最小值。最大值和最小值的差+1,就是开辟数组的大小。
void CountingSort(vector<int>& nums, int max_num, int min_num) {
//max_num为待排序数组的最大值,min_num为待排序数组的最小值
int size = max_num - min_num + 1;
vector<int> count(size);
for(int i = 0; i < nums.size(); i++){
count[nums[i] - min_num]++;
// 按照原始方法就是 count[nums[i]]++,但是优化处理做了一个简单的映射
}
int index = 0;
for (int i = 0; i < size; i++) {
while (count[i] > 0) {
nums[index++] = i + min_num;
// 按照原始方法就是nums[index++] = i,但是优化处理做了一个简单的映射
count[i]--; // 计数完一次,就减少次数
}
}
}
快速排序
顾名思义快排就是最快的。
我们这都是以升序来考虑
任取待排序元素序列中的某元素作为基准值,按照该基准值将待排序列分为两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,左右子序列的分隔元素就是基准值,然后左右序列重复该过程,直到所有元素都排列在相应位置上为止。
这一过程有三种方法实现:
1、Hoare版本
2、挖坑法
3、前后指针法
Hoare版本
对区间左右端点任取一个值作为key(基准值)。
比如取右端点作为key,从左端点往右找比key大的值,然后右端点往左找比key小的值。两边找到后就交换值,最后结束的时候,两个指针指向一个位置,然后将这个位置和key的位置交换。注意:这里先后顺序很重要。
假如取左端点作为key,从右端点往左找比key小的值,然后左端点往右找比key大的值。同样的,顺序也很重要
最后的单趟排序之后区间的情况:[前部分] [key的值] [后部分],前后的子序列中间的分割是key,前部分的值均小于后部分。
代码如下:
void PartSort_1(int* a, int left, int right)
{
// 取右端点为key
int key = right;
while (left < right)
{
while (left < right && a[left] <= a[key])
{
++left;
}
while (left < right && a[right] >= a[key])
{
--right;
}
Swap(&a[left], &a[right]);
}
Swap(&a[right], &a[key]);
}
void PartSort_1(int* a, int left, int right)
{
// 取左端点为key
int key = left;
while (left < right)
{
while (left < right && a[right] >= a[key])
{
--right;
}
while (left < right && a[left] <= a[key])
{
++left;
}
Swap(&a[left], &a[right]);
}
Swap(&a[right], &a[key]);
}
挖坑法
和第一个单次排序一样,需要先选定一个key做为坑。左右端点都可以,但是顺序和hoare版本一样。
void PartSort_2(int* a, int left, int right)
{
// 第一个坑挖在右端点
int key = a[right];
int hole = right;
while (left < right)
{
// 左边找大,填在右边坑
while (left < right && a[left] <= key)
{
++left;
}
a[hole] = a[left];
hole = left;
// 右边找小,填在左边坑
while (left < right && a[right] >= key)
{
--right;
}
a[hole] = a[right];
hole = right;
}
a[hole] = key;
}
void PartSort_2(int* a, int left, int right)
{
int key = a[left];
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 PartSort_3(int* a, int left, int right)
{
int prev = left;
int cur = left + 1;
int key = left;
while (cur <= right)
{
// 寻找比key大的值
if (a[cur] < a[key] && ++prev != cur)
{
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[key], &a[prev]);
//return prev;
}
三数取中
为什么三数取中呢?当一个数组是已经有序的,假如选择第一个位置或最后一个位置的值,那么就找不到比该位置小或大的值,这样的话时间复杂度就达到了O(n^2)。那怎样选择才会规避这种情况呢?那么可以在这个区间呢取一个不是最大也不是最小的数,使用三数取中的方法就可以达到。
整体的代码实现
当一个待排序的数组已经是有序的,那么快排的时间复杂度就会到O(n^2)。
在确定基准值key之前,先求一下待排序的靠中间的数,这样就会避免是最大或最小的数。这个函数实现是GetMidIndex
//获取中位数
int GetMidIndex(int* a, int left, int right)
{
int mid = left + (right - left) / 2;
if (a[left] < a[mid])
{
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 left;
}
else
{
return right;
}
}
}
// Hoare版本的单趟排序
int PartSort_1(int* a, int left, int right)
{
int mid = GetMidIndex(a, left, right);
Swap(&a[right], &a[mid]);
int key = right;
while (left < right)
{
while (left < right && a[left] <= a[key])
{
++left;
}
while (left < right && a[right] >= a[key])
{
--right;
}
Swap(&a[left], &a[right]);
}
Swap(&a[right], &a[key]);
return right;
// return left 也可以因为单趟排序之后,最后right,left指向同一个地方
}
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
int div = PartSort_1(a, begin, end);
QuickSort(a, begin, div - 1);
QuickSort(a, div + 1, end);
}
性能分析
时间复杂度:O(n * log n)
空间复杂度:O(log n)
递归实现的深度为log n。
快排的非递归实现
void QuickSortNonR(int* a, int begin, int end)
{
stack<int> st;
st.push(begin);
st.push(end);
while (!st.empty())
{
int right = st.top();
st.pop();
int left = st.top();
st.pop();
int key = PartSort_1(a, left, right);
if (left < key - 1)
{
st.push(left);
st.push(key - 1);
}
if (key + 1 < right)
{
st.push(key + 1);
st.push(right);
}
}
}
递归和非递归的区别
使用递归会不断调用函数,而调用函数会创建函数栈帧,而快排使用递归需要分割区间到只有一个元素,假如数组非常大,就需要创建很多的栈帧,那么就会有栈溢出的风险,而使用非递归的方法,在堆上开辟空间,那么就可以规避这种风险。
栈的空间是M级别的空间(大概8M,根据不同的机器有不同的大小),而在堆区的空间是G级别的空间。
小区间优化
随着递归的深度不断深入,每一层的递归次数会以2倍的系数快速增加。为了减少递归的最后几层递归,我们可以增加一个判断语句,当元素的个数小于一定数量(小于15),就可以使用其他排序(比如希尔排序),这样就可减少递归的深度,在一定程度上可以优化快速排序的性能。待排序的数组越长,优化效果越明显。
//获取中位数
int GetMidIndex(int* a, int left, int right)
{
int mid = left + (right - left) / 2;
if (a[left] < a[mid])
{
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 left;
}
else
{
return right;
}
}
}
// Hoare版本的单趟排序
int PartSort_1(int* a, int left, int right)
{
int mid = GetMidIndex(a, left, right);
Swap(&a[right], &a[mid]);
int key = right;
while (left < right)
{
while (left < right && a[left] <= a[key])
{
++left;
}
while (left < right && a[right] >= a[key])
{
--right;
}
Swap(&a[left], &a[right]);
}
Swap(&a[right], &a[key]);
return right;
// return left 也可以因为单趟排序之后,最后right,left指向同一个地方
}
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
if(end - begin + 1 < 15)
{
ShellSort(a + begin, end - begin + 1);
}
else
{
int div = PartSort_1(a, begin, end);
QuickSort(a, begin, div - 1);
QuickSort(a, div + 1, end);
}
}
归并排序
归并排序就是将一个待排序的数组不断分割,直到分割到只有一个元素,那么就进行合并。这个合并是合并在一个新开辟的数组temp上,然后在复制到原数组上。
void _MergeSort(int* a, int left, int right, int* temp)
{
if (left >= right) // 当只有一个元素或没有元素就返回
{
return;
}
int mid = left + (right - left) / 2;
_MergeSort(a, left, mid, temp); // 对左边归并
_MergeSort(a, mid + 1, right, temp); // 对右边归并
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
int i = left;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
temp[i++] = a[begin1++];
}
else
{
temp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
temp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
temp[i++] = a[begin2++];
}
for (int index = left; index <= right; index++)
{
a[index] = temp[index];
}
}
void MergeSort(int* a, int n)
{
int* temp = new int[n];
_MergeSort(a, 0, n - 1, temp);
}
非递归实现
归并排序的非递归实现并不需要借助栈来实现,只需要控制每次参与合并的元素个数即可。
可以使用一个gap来控制每次参与合并元素的个数,但是使用gap来控制元素个数,会产生一些区间错误。
情况一:
当最后一组区间合并时,第二个区间不存在,直接跳出循环,不在合并
情况二:
当最后一组区间的第一个区间元素不够gap个元素,直接跳出循环,不在合并
情况三:
当最后一组区间的第二个小区间的元素不够gap个元素,则将第二个小区间的右端点更新为待排序数组的右端点。
void _MergeSortNonR(int* a, int begin1, int end1, int begin2, int end2, int* temp2)
{
int i = begin1;
int j = begin1;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
temp2[i++] = a[begin1++];
}
else
{
temp2[i++] = a[begin2++];
}
}
// 当两个小区间合并完之后,有一个小区间的元素还有剩余,直接把它添加到temp数组之中
while (begin1 <= end1)
{
temp2[i++] = a[begin1++];
}
while (begin2 <= end2)
{
temp2[i++] = a[begin2++];
}
// copy数组
for (; j <= end2; j++)
{
a[j] = temp2[j];
}
}
void MergeSortNonR(int* a, int n)
{
int* temp2 = new int[n];
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;
//规避区间错误,调整区间端点。
if (begin2 >= n)
{
break;
}
if (end2 >= n)
{
end2 = n - 1;
}
_MergeSortNonR(a, begin1, end1, begin2, end2, temp2);
}
gap *= 2;
}
}
性能分析
时间复杂度:O(n * log n)
空间复杂度:O (n)