我在之前的博客中写过归并排序和快速排序的递归实现,最近了解到了归并排序和,快速排序的非递归实现,写下这篇博客检测自己是否还有不懂的地方。
快速排序的递归实现
首先要知道快速排序的递归实现思路是先确定一个key,然后使用挖坑法,双指针法或是前后指针法,将数组分为三个部分,将key放到中间,然后递归左半部分和右半部分,递归结束的条件也就是当递归的区域不存在时或递归区域为一个数时停止。
双指针法实现
首先双指针法的思路也就是使用两个指针一个指向的是左端一个指向的是右端,然后选取key,一般而言key的选取是最左端或是最右端的数据。对于双指针法而言你选取key的位置不同你下面要进行的处理也是不同的。如果你选取的key在最左端,那么你就要先从右边选取小于key的值,再从左边选取大于key的值,最后在交换,反之则相反,那如果你选择了最左端当作你的key然后继续先从左边选取大于key的值,再从右边选取小于key的值会造成什么样的后果呢?画图解释
下面我来写一下双指针法实现第一趟排序
int partsort1(int* nums, int begin, int end)
{
int keyi = begin;
int left = begin;
int right = end;
while (left < right)
//选取最左端做key所以要先从右端开始找
{
while (left < right && nums[right] >= nums[keyi])
{
right--;
}//注意这里必须先确定left<right并且right大于等于keyi所代表的值,才能进行--
//否则如果遇到全是keyi的数组(即全是相同数字的数组,就会出现越界),
while (left < right && nums[left] < nums[keyi])
{
left++;
}
swap(&nums[left], &nums[right]);
}
return left;//最后返回keyi所在正确位置的值
}
void quicksort1(int* nums, int begin, int end)
{
if (begin >= end)
{
return;
}//当要递归的数组范围为错误时,递归结束
int mid = partsort1(nums, begin, end);
quicksort1(nums, begin, mid - 1);//递归处理左半部分
quicksort1(nums, mid + 1, end);//递归处理右半部分
}
主函数:
int main()
{
int arr[] = { 1,4,7,8,5,2,9,6,3 };
int len = sizeof(arr) / sizeof(arr[0]);
quicksort1(arr,0,len - 1);
printf_arr(arr, len);
return 0;
}
挖坑法实现
挖坑法的思想和双指针法其实很相似,依旧是先选取key,但是这次储存的是key这个值而不是keyi,保存了key之后,key所在的位置也就是第一个坑了,这里依旧是你选取的坑在左端则从右边开始选,反之相反。直到最后
下面是代码实现
int partsort2(int* nums, int begin, int end)
{
int key = nums[begin];
int Hole = begin;//确定坑的位置
int left = begin;
int right = end;
while (left < right)
{
while (left<right&&nums[right] >= key)
{
right--;
}
nums[Hole] = nums[right];//填坑
Hole = right;//更新坑的位置
while (left < right && nums[left] <= key)
{
left++;
}
nums[Hole] = nums[left];
Hole = left;
}
//最后将Key填入
nums[left] = key;
return left;//最后返回key的位置
}
void quicksort1(int* nums, int begin, int end)
{
if (begin >= end)
{
return;
}//当要递归的数组范围为错误时,递归结束
int mid = partsort2(nums, begin, end);
quicksort1(nums, begin, mid - 1);//递归处理左半部分
quicksort1(nums, mid + 1, end);//递归处理右半部分
}
主函数依旧不变
前后指针法
前后指针法的实现思路就不和上面的两种方法一样了。首先依旧是选择key并且记录keyi的位置。
然后使用prev和cur指针,其中prev指针指向最左端,cur指向prev+1的位置。如果cur指向的那个位置的值是小于key的那就会先让prev+1再交换cur和prev指针的值。如果是大于key的值那就直接让cur++即可。
代码实现:
int partsort3(int* nums, int begin, int end)
{
int keyi = begin;//选择最左端作为基准值
int cur = begin + 1;
int prev = begin;
while (cur <= end)
{
if (nums[cur] < nums[keyi]&&++prev!=cur)
{
swap(&nums[cur], &nums[prev]);
}
cur++;
}
swap(&nums[prev], &nums[keyi]);
return prev;
}
快速排序的优化
快速排序还是可以优化的,优化的地方也就是在选择基准数上,假设传给快速排序的数组本身就是一个有序的数组,那么如果依旧是选择最左端或是最右端当作基准数,那快排的时间复杂度就会变成o(n^2)
原因如下图:
这只是一小部分,不要认为这里的时间复杂度为o(n),因为每寻找一个数的正确位置就要完全遍历一遍数组,所以时间复杂度为O(n^2)。那么如何优化这种情况呢?使用的方法也就是三数取中法。
三数取中法,顾名思义也就是先从数组中选取三个数(一般是选择数组开头中间和末尾的三个数),选择这三个数中大小中等的数。
下面是代码的实现:
int Get_Mid(int* arr, int left, int right)
{
int mid = (left + right) >> 1;//找到中间下标
if (arr[mid] > arr[left])
{
if (arr[mid] < arr[right])
{
return mid;
}
else if (arr[left] > arr[right])
{
return right;
}
else
return left;
}
else
{
if (arr[mid] > arr[right])
{
return mid;
}
else if (arr[left] < arr[right])
{
return left;
}
else
return right;
}
}
最后这个函数返回了中间数的下标,再将最左端的元素和得到的这个元素进行交换,由此完成。
但是即使这样优化了对于力扣上的这一道题使用快排依旧不能通过。
题目链接:912. 排序数组 - 力扣(Leetcode)
因为力扣后台给了一个应用示例为很多个相同的元素,最终导致了时间超时。
为了处理这种情况就要将快排优化为三向切分快速排序,在上面的处理一趟的快排时遇到值等于基准元素的元素时,是直接跳过的最终就导致了等于基准值的元素既可以放到左边又可以放到右边。而三向切分快速排序最终会将小于基准值的元素放到左边,大于基准值得元素放到右边,等于基准值得元素放到中部。实现得思路也和前后指针法很相似
下面是代码:
void quicksort2(int* nums, int begin, int end)
{
if (begin >= end)
{
return;
}
//第一步选择基准数
int mid = GetMid(nums, begin, end);
swap(&nums[mid], &nums[begin]);
int prev = begin;
int cur = begin + 1;
int right = end;
int key = nums[begin];
while (cur <= right)
{
if (nums[cur] < key)
{
swap(&nums[prev], &nums[cur]);
prev++;
cur++;
}
else if (nums[cur] > key)
{
swap(&nums[cur], &nums[right]);
right--;
}
else
{
cur++;
}
}//当循环完成得时候c和r中间得也就是等于key的值
quicksort2(nums, begin, prev - 1);
quicksort2(nums, right + 1, end);
}
当然对于力扣那道题最后你还需要优化一下三数取中,让取的其中一个数是一个随机数,因为即使你使用了三向划分,力扣后台仍旧存在针对种写法的代码,那就是直接针对于我们选取中间开头和结尾三个数做三数取中。所以你要让三数取中的其中一个数随机选取就可以通过上面力扣的题目
下面附上我写的代码:
/**
* Note: The returned array must be malloced, assume caller calls free().
*/
void swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
int GetMid(int* a, int left, int right)//三数取中法
{
int mid = (left + rand()%(right)) / 2;//随机选取了
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;//mid大于left但是小于right所以mid为中间
}
else if (a[left] > a[right])//这里mid大于right那么mid就是三者中最大的数,下面只需要在left和right中取大的按个即可
{
return left;
}
else
{
return right;
}
}
else
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] > a[right])//在这里mid为最小
{
return left;
}
else
{
return right;
}
}
}
void quicksort(int* nums, int begin,int end)
{
if (begin >= end)
{
return;
}
//快速排序的三向切分法使用了三个指针left,cur和right(以下简称为l,r和c)
//让l指向左边即keyi,r指向n-1
//c指向l+1
//情况1:如果c指向的那个数字小于key那么就让其和l所指向值交换,并且让c和l++
//情况2:如果c指向的那个数字大于key那就让其和r所指向的值交换但是不移动c只让r--因为只知道r交换值后所指向的那个为大于key的值
//但是不知道原先r指向得那个值是大于还是小于key的
//情况3:如果c指向的值是等于key的那就直接让c++
//当cur>r时循环停止,这样l和r之间的元素为等于key的元素,两边分别为大于和小于key的元素
int left = begin;
int cur = left + 1;
int right = end;
int mid = GetMid(nums, begin, end);
swap(&nums[begin], &nums[mid]);
int key = nums[left];
while (cur <= right)
{
if (nums[cur] < key)//情况1
{
swap(&nums[cur], &nums[left]);
left++;
cur++;
}
else if (nums[cur] > key)//情况二
{
swap(&nums[right], &nums[cur]);
right--;
}
else//情况三
{
cur++;
}
}
quicksort(nums, begin, left - 1);
quicksort(nums, right + 1, end);
}
int* sortArray(int* nums, int numsSize, int* returnSize){
srand(time(0));
quicksort(nums,0,numsSize - 1);
*returnSize = numsSize;
return nums;
}
快速排序的非递归实现
但是对于递归有一个很大的缺陷就是当递归层次过深的时候就会造成栈溢出。
那么如果不能使用递归又要怎么去实现呢?那么这里需要使用一个数据结构去储存范围来实现递归的模拟实现递归,这个数据结构就是栈。栈的后进先出特点能够去模拟递归的实现。
因为c没有栈所以我事先在我的代码中加入了一个栈,但是因为栈的代码很多所以我就不写出来了。
画图解释:
代码实现:
void quicksortNor(int* nums, int begin, int end)
{
//既然要用栈去模拟实现,首先就要有一个栈
Stack st;
StackInit(&st);//初始化栈
//递归的第一步是将整个数组进行递归所以这里我也就先将整个的数组1放进栈中,这里是将整个数组的范围放到数组中去
StackPush(&st, begin);
StackPush(&st, end);
while (!StackEmpty(&st))//当栈中还有元素的时候循环继续
{
int end1 = StackTop(&st);//取出顶部的元素,这里顶部的元素也就是要处理数组的尾下标
StackPop(&st);
int begin1 = StackTop(&st);
StackPop(&st);
int mid = partsort3(nums, begin1, end1);//处理这一段数组
//下面要将mid左半段和有半段放到栈中
//为了模拟递归这里先放右半段再放左半段
if (mid + 1 < end1) {
StackPush(&st, mid + 1);
StackPush(&st, end1);
}//当数组范围违法时不能再放到栈中
//下同
if (mid - 1 > begin1) {
StackPush(&st, begin1);
StackPush(&st, mid - 1);
}
}
StackDestroy(&st);//最后销毁这个栈
}
归并排序的递归实现
首先对于一个数组如果把它分为两个部分如果左半部分有序,右半部分有序,将这两个数组合并起来就能够得到一个有序的数组,而归并排序的思路也就是这样,先将所有的元素不断分割为单独的一个元素(单独的元素默认为有序),再两两合并。最后将整个数组合并
思维图:
除此之外还需要一个额外的数组,这个额外数组再合并的时候就用于放置值,最后复制回原数组。
合并的方法也就是选择左右区间小的那个放到临时数组中,最后将临时数组放到原数组中去。
代码实现:
void _MergeSort(int* nums, int* tmp, int begin, int end)//左闭右闭的区间
{
if (begin == end)
{
return;
}//确定递归结束的窗口
int mid = (begin + end) / 2;
_MergeSort(nums, tmp, begin ,mid);//继续分解左半部分
_MergeSort(nums, tmp, mid + 1, end);//继续分解右半部分
//下面开始合并
int begin1 = begin;
int end1 = mid;
int begin2 = mid + 1;
int end2 = end;
int j = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (nums[begin1] < nums[begin2])
{
tmp[j++] = nums[begin1++];
}
else
{
tmp[j++] = nums[begin2++];
}
}
//可能会出现左半或是右半区域还存在元素的情况需要放到临时数组中去
while (begin1 <= end1)
{
tmp[j++] = nums[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = nums[begin2++];
}
//最后复制回原数组
//因为每一次复制的范围不同,所以复制回原数组的开始点也是不同的
//可能会出现复制回原数组的是下标为3到5的元素,所以需要要控制开始的地点
memcpy(nums + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
void MergeSort(int* nums, int n)
{
//首先创建一个临时数组
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail:");
return;
}
_MergeSort(nums, tmp, 0, n - 1);
}//不能使用这个函数去直接递归,不然每递归一次都会创建一个新数组。
但是归并排序的递归实现依旧存在可能出现递归过深导致栈溢出的情况,所以也就有了归并排序的非递归实现。
归并排序的非递归实现
和快速排序的非递归实现不同,归并排序的非递归实现不需要使用栈或队列直接使用一个循环就可以解决。画图表示:
下面是代码实现:
void MergeSortNor1(int* nums, int n)
{
//依旧需要先创建一个临时数组
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
int gap = 1;//确定每组的元素个数
while (gap < n) {
for (int i = 0; i < n; i+=2*gap)//记住每一次跳过的是一个合并的组,再去下一个组
{
//下面就要确定左右区域的开始和结束区间了
int begin1 = i; int end1 = i + gap - 1;
int begin2 = i + gap; int end2 = i + 2 * gap - 1;
int j = begin1;
while (begin1 <= end1 && begin2 <= end2)
{
if (nums[begin1] < nums[begin2])
{
tmp[j++] = nums[begin1++];
}
else
{
tmp[j++] = nums[begin2++];
}
}
//可能会出现左半或是右半区域还存在元素的情况需要放到临时数组中去
while (begin1 <= end1)
{
tmp[j++] = nums[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = nums[begin2++];
}
}
//最后整个复制回原数组
memcpy(nums, tmp, sizeof(int) * (n));//整段复制回原数组
gap *= 2;
}
}
这一个代码能够解决一部分的代码,那部分代码的条件便是数组的元素数量为2的次方个。
首先要知道在数组的数量不是2的n次方的情况下,造成错误的原因。
假设要排序的数组的数量为9,
那么针对这三种越界的方式,解决方法一:对于越界的begin2 和end2让其不进入下面的循环就能防止越界。也就是当begin2和end2越界的时候,直接break跳出循环,但是若使用这种解决办法那么当tmp数组返回复制给原数组就不能使用整个数组一起赋值的代码。而是要归并一段复制一段。
下面是代码实现:
void MergeSortNor2(int* nums, int n)
{
//依旧需要先创建一个临时数组
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
int gap = 1;//确定每组的元素个数
while (gap < n) {
int j = 0;
for (int i = 0; i < n; i += 2 * gap)//记住每一次跳过的是一个合并的组,再去下一个组
{
//下面就要确定左右区域的开始和结束区间了
int begin1 = i; int end1 = i + gap - 1;
int begin2 = i + gap; int end2 = i + 2 * gap - 1;
if (begin2 >= n || end1 >= n)
{
break;
}//对于begin2或是end2越界那就直接让其跳出循环即可
if (end2 >= n)
{
//处理第三种情况如果只有end2越界那就直接
end2 = n - 1;
}
while (begin1 <= end1 && begin2 <= end2)
{
if (nums[begin1] < nums[begin2])
{
tmp[j++] = nums[begin1++];
}
else
{
tmp[j++] = nums[begin2++];
}
}
//可能会出现左半或是右半区域还存在元素的情况需要放到临时数组中去
while (begin1 <= end1)
{
tmp[j++] = nums[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = nums[begin2++];
}
//最后归并回原数组
memcpy(nums+i, tmp+i, sizeof(int) * (end2 - i+1));//归并一段复制一段
//那么复制回去的元素肯等每一次都不是从固定的位置开始的。每次复制的元素个数也就是end2 - i(不能)
//不能使用begin1因为begin1已经被移动了。
//而每一次开始复制的位置自然也就是+
}
gap *= 2;
}
}
还有一种解决方式也就是和调整end2一样去调整begin2和end1,如果使用这种方法那么就可以采用整段数组复制的方式。
代码实现:
void MergeSortNor3(int* nums, int n)
{
//依旧需要先创建一个临时数组
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
int gap = 1;//确定每组的元素个数
while (gap < n) {
int j = 0;
for (int i = 0; i < n; i += 2 * gap)//记住每一次跳过的是一个合并的组,再去下一个组
{
//下面就要确定左右区域的开始和结束区间了
int begin1 = i; int end1 = i + gap - 1;
int begin2 = i + gap; int end2 = i + 2 * gap - 1;
if (end1 >= n)//情况一:end1越界
{
end1 = n - 1;
//为了不让begin2和end2进去循环所以要让begin2和end2,指向一段不存在的下标范围
begin2 = n + 1;
end2 = n;
}
else if (begin2 >= n)//begin2和end2越界
{
end1 = n - 1;
begin2 = n + 1;
end2 = n;
}
else if(end2>=n)//end2越界
{
end2 = n - 1;
}
while (begin1 <= end1 && begin2 <= end2)
{
if (nums[begin1] < nums[begin2])
{
tmp[j++] = nums[begin1++];
}
else
{
tmp[j++] = nums[begin2++];
}
}
//可能会出现左半或是右半区域还存在元素的情况需要放到临时数组中去
while (begin1 <= end1)
{
tmp[j++] = nums[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = nums[begin2++];
}
//最后归并回原数组
}
memcpy(nums, tmp, sizeof(int) * (n));//整段复制
gap *= 2;
}
}
如果您发现了任何错误,恳请您能指出。
希望这篇博客能对你有所帮助。