我在之前的博客中写过归并排序和快速排序的递归实现,最近了解到了归并排序和,快速排序的非递归实现,写下这篇博客检测自己是否还有不懂的地方。
快速排序的递归实现
首先要知道快速排序的递归实现思路是先确定一个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;
}
快速排序的非递归实现
但是对于递归有一个很大的缺陷就是当递归层次过深的时候就会造成栈溢出。
那么如果不能使用递归又要怎么去实现呢?那么这里需要使用一个数据结构去储存范围来实现递归的模拟实现递归,这个数据结构就是栈。栈的后进先出特点能够去模拟递归的实现。
因为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;
}
}
希望这篇博客能对你有所帮助。