[Algorithm] Based on the three ideas of hoare quicksort and non-recursive, benchmark value selection optimization [In-depth analysis of quicksort - super detailed comments and explanations] Have you really learned quicksort completely?

insert image description here

foreword

It's not easy to get used to typing after liking it first. This is all done with great care. I hope to get support from you. The likes and support are a very important motivation for me. Don't forget to follow me after reading it! ️️️

We all know that sorting algorithm is an extremely important algorithm in programming high-level languages. Among them, the more important is the quick sort. But in the process of learning quick sort, many people do not understand quick sort well. In fact, there are three ideas of quick sort .

  • hoare version
  • Digging
  • back-and-forth pointer method

For the functions in the C language, qsort()the blogger gave a detailed introduction in the early blog, and the partners who haven't used it yet can eat it through the portal.

For friends who are not quite clear about the ranking of the nine majors, bloggers here provide a summary portal for everyone.

There is also a blog, which is the source code of the complexity test of the sorting algorithm. The blogger also provides it to you here. We will use it later in the following learning process.


Then the bloggers here will first write some columns full of dry goods!

Data Structure Column: Data Structure This contains a lot of bloggers' summaries of data structure learning. Each article is written with great care. Interested partners can support it!
Algorithm Column: Algorithm This can be said to be the blogger's writing process. It summarizes some classic questions and the algorithm implementation, which is very helpful for exams and competitions!
Likou Brush Question Column: Leetcode wants to hit the partners of ACM, Blue Bridge Cup or college student programming competition. Here are the blogger's brush questions record, I hope it will help you!
C's deep anatomy column: C language deep anatomy For beginners who want to deeply learn the wisdom contained in C language and the underlying implementation of various functions, I believe this column will be helpful to you!
In order to facilitate the testing of partners, the blogger here provides a main()source code of the function used for testing, and you can directly copy it for testing when testing.


This article uses some C++ wrapper functions, such as swap, max, etc.
So the reader remembers to add the required IO, algorithm and other header files

The programming language used in this blog is C++, but, in order to allow partners who only know C language at present to master the algorithm core,本篇博客没有使用C++包装的vector,全部使用了数组来进行演示。

The code in this blog implements intthe ascending order of type data. Partners who want to achieve descending order or other data type sorting can make modifications on this basis. Our goal in this article is to understand the algorithm idea.

Note: The animations in this blog are all from the Internet

What is Quick Sort

The actual implementation of quick sort:

  • Determine one keyvalue at a time (generally select the leftmost value of the interval before optimization)
  • Arrange keythis value. For example , the first number in the original array is 6that the array is in a [1,2,3,4,5,6,7,8,9,10]shuffled order, so if it is used as a key, it should return to the position of the subscript 6after a single pass .65
  • Then recursively keyi(keyi是key的下标)repeat the above steps for the divided subinterval array. That is to say, for [begin,keyi-1]and [keyi+1,end]these two intervals, arrange their keys. In fact, this is a divide and conquer process, similar to the traversal of a binary tree.
  • The non-recursive method is actually to use the data structure stack to simulate the recursive process, and the single-pass sorting is the same.

There are three realization ideas for single-pass sorting:
1. Hoare idea
2. Digging pit method
3. Front and back pointer method


Recursive implementation of quicksort

void _QuickSort(int* a, int begin, int end) {
    
    
	//区间不存在或者只会有一个值不需要再处理
	//快排:每次把key弄好,递归解决key两边的数
	if (begin >= end)return;
	//利用单趟排序找到key并排好,然后得到key的下标keyi
	int keyi = PartSort(a, begin, end);
	//PartSort即为单趟排序
	_QuickSort(a, begin, keyi - 1);
	_QuickSort(a, keyi + 1, end);
}
void QuickSort(int* a, int n) {
    
    
	int begin = 0;
	int end = n - 1;
	_QuickSort(a, begin, end);
}

Tips: Regarding the implementation of PartSort single-pass sorting, I will discuss the three implementation ideas in detail later.

A non-recursive implementation of quicksort

In fact, to put it bluntly, it is to use the data structure stack to simulate the recursive process, which is actually the same as the non-recursive method of binary tree dfs traversal.

#include<stack>
void QuickSortNonR(int* a, int begin, int end) {
    
    
	//1.直接改循环
	//2.用数据结构栈模拟递归过程
	stack<int>st;
	st.push(end);
	st.push(begin);
	//栈里面没有区间了,就结束了
	while (!st.empty()) {
    
    
		int left = st.top();
		st.pop();

		int right = st.top();
		st.pop();
		//利用单趟排序找到key并排好,然后得到key的下标keyi
		int keyi = PartSort(a, left, right);
		//[left,keyi-1]keyi[keyi+1,right]
		
		//怎么迭代
		//先入右再入左

		//栈里面的区间都会拿出来,单趟排序分割,子区间再入栈
		if (left < keyi - 1) {
    
    
			st.push(keyi - 1);
			st.push(left);
		}
		if (keyi + 1 < right) {
    
    
			st.push(right);//同样,先入右再入左
			st.push(keyi + 1);
		}
	}
}
//
void QuickSort(int* a, int n) {
    
    
	int begin = 0;
	int end = n - 1;
	QuickSortNonR(a, begin, end);
}

Detailed explanation of single-pass sorting

hoare thought

  • Let beginfind elements larger than the reference value from front to back, and stop after finding them; let them find elements smaller than the reference value from back to front, and stop after endfinding them; if they do not meet: Swap the element at the position with the element at the position ; Swap the reference value and the element at the position after the loop ends .beginendbeginendbegin

insert image description here

//hoare版本        O(nlogn)
//1.选出一个key,一般是最左边或者最右边的值
//2.单趟排完之后:要求左边比key小,右边比key大
//左边key,右边先走
//右边key,左边先走
//每次排完一趟,key的位置的值就是准确的了,不用动了
int PartSort1(int* a, int begin, int end) {
    
    //hoare
	int left = begin;
	int right = end;
	int keyi = left;//保存下标,也就是指针是最优的
//下面是单趟
	while (left < right) {
    
    //相遇就停下来
		//右边先走,找小
		while (left < right && a[right] >= a[keyi])--right;//while一定要带等号,否则会死循环
		                                               //而且要带多一个调节,否则有可能会越界
		//左边再走,找大
		while (left < right && a[left] <= a[keyi])++left;
		//交换
		swap(a[left], a[right]);
	}
	//交换key和相遇位置换
	swap(a[keyi], a[right]);
//为什么左边做key,要让右边先走?
//因为要保证相遇的位置的值比key小

	keyi = left;
	//[begin,keyi-1]和[keyi+1,end]有序即可
	return keyi;
}

Digging

The digging method is actually just a rewrite of the hoare version, and it has not actually changed in the real sense.
The following is the idea of ​​​​digging a pit:
insert image description here

//挖坑法
//和hoare相同的地方就是-排完单趟-做到key左边比key小,右边比key大
int PartSort2(int* a, int begin, int end) {
    
    
	int key = a[begin];
	int piti = begin;//begin是第一个坑
	int left = begin;
	int right = end;
	while (left < right) {
    
    
		//右边找小,填到左边的坑
		while (left < right && a[right] >= key) {
    
    
			right--;
		}
		a[piti] = a[right];
		piti = right;//自己变成坑
		while (left < right && a[left] <= key){
    
    
			left++;
		}
		a[piti] = a[left];
		piti = left;
	}
	//一定相遇在坑的位置
	a[piti] = key;
	return piti;
}

back-and-forth pointer method

Define two pointers, prevsum cur, and define keythe value again. The curpointer starts from the leftbeginning. If it encounters a keylarger one, it will be filtered out. If it is keysmaller, it will stop. , prev++and judge whether the prevsum curis equal. If it is not equal, the two values ​​will be exchanged. , the last order is sorted, those smaller than the key are left on the left of the key, and those larger than the key are on the right of the key. representative in the
animationicursprev
insert image description here

//前后指针版
int PartSort3(int* a, int begin, int end) {
    
    
	//排完之后prev之前的比prev小,prev后的比prev大
	int prev = begin;
	int cur = begin + 1;//一开始cur和prev要错开
	int keyi = begin;
	
	while (cur <= end) {
    
    
		//如果cur的值小于keyi的值
		if (a[cur] < a[keyi] && ++prev != cur) {
    
    //只有这种情况要处理一下
			swap(a[prev], a[cur]);
		}
		++cur;
	}
	//
	swap(a[prev], a[keyi]);
	//此时prev的位置就是keyi的位置了
	keyi = prev;
	return keyi;
}

Optimization of Quick Sort

The above three search keymethods, if the array is ordered, the efficiency will be very low:
why, because the efficiency becomes
n+n-1+n-2+n-3.....
so: O(n^2)
when is the efficiency of quick sorting the highest? – When each key is the median of the interval, it is the fastest, so it is a strict divide and conquer, and the efficiency isO(NlogN)

And because recursion is used, if the number is slightly larger, stack overflow will occur.

We can use TestOP to test (see the portal at the beginning of the article for the efficiency test).
If we use Quick Sort to sort order, debugit may explode when the version is small
and the efficiency will become very low.
insert image description here
insert image description here
So how do we optimize it?


Three optimization schemes:

  • Pick a random number as the keyvalue
  • three-digit
  • Inter-cell optimization

The first optimization scheme is well understood, and here we focus on the following two methods for optimization.

three-digit

The meaning of taking the middle of the three numbers is to select the first number in the interval, the middle number, and the last number that is not too small or not as the key value of the interval.

//优化-三数取中
int GetMidIndex(int* a, int begin, int end) {
    
    
	int mid = (begin + end) / 2;
	if (a[begin] < a[mid]) {
    
    
		if (a[mid] < a[end]) {
    
    
			return mid;
		}
		else if (a[begin] < a[end]) {
    
    
			return end;
		}
		else {
    
    
			return begin;
		}
	}
	else {
    
    //a[begin]>a[mid]
		if (a[mid] > a[end]) {
    
    
			return mid;
		}
		else if (a[begin] < a[end]) {
    
    
			return begin;
		}
		else {
    
    
			return end;
		}
	}
}

Inter-cell optimization

When the interval is relatively small, it is no longer recursively divided to sort the small interval. Other sorts can be considered.
It is recommended to use insertion sort directly.

Here we just need to _QuickSort()make adjustments to the function just now.

void _QuickSort(int* a, int begin, int end) {
    
    
	//记录递归次数
	callCount++;
	//区间不存在或者只会有一个值不需要再处理
	//快排:每次把key弄好,递归解决key两边的数
	if (begin >= end)return;
	//小区间优化
	if (end - begin > 10) {
    
    //当区间大于10的时候,继续递归
		int keyi = PartSort3(a, begin, end);//每一个partsort负责找到key
		_QuickSort(a, begin, keyi - 1);
		_QuickSort(a, keyi + 1, end);
	}
	else {
    
    //当区间较小的时候,直接使用插入排序
		InsertSort(a + begin, end - begin + 1);//注意+1和a+begin
	}
}

quicksort overall code

In the complete code, only PartSort3()the three numbers that are used are taken, and the other one is arranged in a single pass. It can also be used directly, just keyreplace it, it is very simple.

//优化-三数取中
int GetMidIndex(int* a, int begin, int end) {
    
    
	int mid = (begin + end) / 2;
	if (a[begin] < a[mid]) {
    
    
		if (a[mid] < a[end]) {
    
    
			return mid;
		}
		else if (a[begin] < a[end]) {
    
    
			return end;
		}
		else {
    
    
			return begin;
		}
	}
	else {
    
    //a[begin]>a[mid]
		if (a[mid] > a[end]) {
    
    
			return mid;
		}
		else if (a[begin] < a[end]) {
    
    
			return begin;
		}
		else {
    
    
			return end;
		}
	}
}
//hoare版本        O(nlogn)
int PartSort1(int* a, int begin, int end) {
    
    //hoare
	int left = begin;
	int right = end;
	int keyi = left;//保存下标,也就是指针是最优的
//下面是单趟
	while (left < right) {
    
    //相遇就停下来
		//右边先走,找小
		while (left < right && a[right] >= a[keyi])--right;//while一定要带等号,否则会死循环
		                                               //而且要带多一个调节,否则有可能会越界
		//左边再走,找大
		while (left < right && a[left] <= a[keyi])++left;
		//交换
		swap(a[left], a[right]);
	}
	//交换key和相遇位置换
	swap(a[keyi], a[right]);
	keyi = left;
	//[begin,keyi-1]和[keyi+1,end]有序即可
	return keyi;
}
//挖坑法
int PartSort2(int* a, int begin, int end) {
    
    
	int key = a[begin];
	int piti = begin;//begin是第一个坑
	int left = begin;
	int right = end;
	while (left < right) {
    
    
		//右边找小,填到左边的坑
		while (left < right && a[right] >= key) {
    
    
			right--;
		}
		a[piti] = a[right];
		piti = right;//自己变成坑
		while (left < right && a[left] <= key){
    
    
			left++;
		}
		a[piti] = a[left];
		piti = left;
	}
	//一定相遇在坑的位置
	a[piti] = key;
	return piti;
}
//前后指针版
int PartSort3(int* a, int begin, int end) {
    
    
	//排完之后prev之前的比prev小,prev后的比prev大
	int prev = begin;
	int cur = begin + 1;//一开始cur和prev要错开
	int keyi = begin;

	//加入三数取中的优化
	int midi = GetMidIndex(a, begin, end);
	swap(a[keyi], a[midi]);//这样换一下,key就变成GetMidIndex()函数的取值了-后面的代码都不变了

	while (cur <= end) {
    
    
		//如果cur的值小于keyi的值
		if (a[cur] < a[keyi] && ++prev != cur) {
    
    //只有这种情况要处理一下
			swap(a[prev], a[cur]);
		}
		++cur;
	}
	swap(a[prev], a[keyi]);
	keyi = prev;
	return keyi;
}
void _QuickSort(int* a, int begin, int end) {
    
    
	//区间不存在或者只会有一个值不需要再处理
	//快排:每次把key弄好,递归解决key两边的数
	if (begin >= end)return;
	//小区间优化
	if (end - begin > 10) {
    
    
		int keyi = PartSort3(a, begin, end);//每一个partsort负责找到key
		_QuickSort(a, begin, keyi - 1);
		_QuickSort(a, keyi + 1, end);
	}
	else {
    
    
		InsertSort(a + begin, end - begin + 1);//注意+1和a+begin
	}
}
void QuickSortNonR(int* a, int begin, int end) {
    
    
	//1.直接改循环
	//2.用数据结构栈模拟递归过程
	stack<int>st;
	st.push(end);
	st.push(begin);
	//栈里面没有区间了,就结束了
	while (!st.empty()) {
    
    
		int left = st.top();
		st.pop();

		int right = st.top();
		st.pop();

		int keyi = PartSort3(a, left, right);
		//[left,keyi-1]keyi[keyi+1,right]
		
		//怎么迭代
		//先入右再入左

		//栈里面的区间都会拿出来,单趟排序分割,子区间再入栈
		if (left < keyi - 1) {
    
    
			st.push(keyi - 1);
			st.push(left);
		}
		if (keyi + 1 < right) {
    
    
			st.push(right);//同样,先入右再入左
			st.push(keyi + 1);
		}
	}
}
void QuickSort(int* a, int n) {
    
    
	int begin = 0;
	int end = n - 1;
	_QuickSort(a, begin, end);//这里也可以改成用非递归那个函数
	//QuickSortNonR(a, begin, end);
}

end

Seeing this, I believe that my partners have a relatively in-depth understanding of Quick Sort. In this Quick Sort, in fact, the wisdom of many people has penetrated. We have mastered part of this sorting knowledge, and more importantly, we have mastered the subtle algorithms inside. ideas.
If this blog is helpful to you, don't forget to like, follow and bookmark!

Guess you like

Origin blog.csdn.net/Yu_Cblog/article/details/125766452