[Data Structure--Hand-Torn Sorting Algorithm Part 6] Recursively Realize Quick Sort (Integrating Hall's version, digging method, front and back pointer method into one implementation method, very good at typing)

Table of contents

1. Common sorting algorithms

1.1 Basic idea of ​​exchange sorting

2. Implementation method of quick sort

2.1 Basic idea

3 hoare (Hall) version

3.1 Implementation ideas

3.2 Diagram of ideas

3.3 Why steps 2 and 3 of the realization idea cannot be exchanged

3.4 hoare version code implementation

3.5 hoare version code test

4. Digging method

4.1 Implementation ideas

4.2 Diagram of thinking

4.3 Code implementation of digging method

4.4 Digging method code test

5. Front and rear pointer versions

5.1 Implementation ideas

5.2 Diagram of thinking

5.3 Code implementation of front and back pointer method

5.4 Code test before and after the pointer method

6. Time complexity analysis

6.1 Best case

6.2 Worst case

7. Optimized quick sort

7.1 Key selection optimization

7.2 Inter-cell optimization


1. Common sorting algorithms

1.1 Basic idea of ​​exchange sorting

Bubble sorting is one of the exchange sorting, let's first understand the following ideas of bubble sorting.

Basic idea: The so-called exchange is to exchange the positions of the two records in the sequence according to the comparison result of the key values ​​of the two records in the sequence. Records with smaller key values ​​are moved to the front of the sequence.

2. Implementation method of quick sort

The recursive implementation is very similar to the preorder traversal of the binary tree. There are three common ways to divide the interval into left and right halves: 1. hoare version; 2. digging method; 3. left and right pointer method.

2.1 Basic idea

Quick sorting is a binary tree structure exchange sorting method proposed by Hoare in 1962. Its basic idea is: any element in the sequence of elements to be sorted is taken
as a reference value, and the set to be sorted is divided into two subsequences according to the sorting code , all elements in the left subsequence are less than the reference value,
all elements in the right subsequence are greater than the reference value, and then the leftmost subsequence repeats the process until all elements are arranged in the corresponding position .

Therefore, a very important point in the back is to go to the left to find the small, and to the right to find the big.

3 hoare (Hall) version

3.1 Implementation ideas

We specify to sort in ascending order , the name of the sorted array is a, the reference value key, the reference value subscript keyi, left left, right right.

1. Select a key. The key can be any element in the array that needs to be sorted. In this article, the key is selected as a[left] ;

2. Go left from the right end (tail) of the array, when a[right] < key(a[keyi]), right stops;

3. Then go right from the left end (head) of the array, when a[left] > key(a[keyi]), left stops;

4. Exchange a[left], a[right];

5. Repeat steps 2, 3, and 4. When left and right go to the same position, exchange the element and key at this position, and the position of the key is determined.

6. At this time, the key divides the array into left and right intervals. We use the first 5 steps for the left and right intervals, and then determine the keys of the left and right intervals again, recurse the left interval, and then recurse the right interval to achieve sorting.

Note: The order of steps 2 and 3 cannot be changed. As for why, we will talk about it after diagramming.

3.2 Diagram of ideas

Our diagrams are drawn in accordance with the realization ideas.

3.3 Why steps 2 and 3 of the realization idea cannot be exchanged

We can see that in the thinking diagram, when 3 is finally found, R will go first, and R will first meet 3. If L will go first, look for the big left, and L will not stop when it meets 3, and will go directly to the position of R , the R position is 9, when they meet at this time, exchange them. Once exchanged, not all the left intervals are smaller than key (6), and an error will occur. Therefore, if the key is selected on the left, the right will go first.

Q: We understand the hoare version. If the key is selected on the right, does the left go first?

A: The answer is yes. This is actually a question of whether L meets R or R meets L. We can still use the above solution to think about this problem. When L meets R, it means that R has stopped, and R stops is a[right]<key. At this time, when L meets R, the meeting position is less than key , exchange (key, a[left]), at this time the last element smaller than key is placed on the far left, and key is at a certain position, no need to adjust again, the left interval with key as the midpoint is all smaller than key , the right interval is all greater than the key;

The key is on the right. When R encounters L, it means that L has stopped. When L stops, it means a[left]>key. At this time, when R meets L, the meeting position is greater than key. Exchange (a[left], key), then the last element greater than the key is placed on the far right, and the key is at a certain position, so there is no need to adjust it again. The left interval with the key as the midpoint is all smaller than the key, and the right interval is all larger than the key.

So if the key is selected on the left, go to the right first; if the key is selected on the right, go to the left first.

3.4 hoare version code implementation

// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
	int keyi = left;
	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 left, int right)
{
	if (left >= right)
		return;

	int keyi = PartSort1(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}
 
 

3.5 hoare version code test

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
void Print(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}
 //快速排序递归实现
 //快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
	int keyi = left;
	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[left], &a[keyi]);
	return left;
}
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int keyi = PartSort1(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}
void test()
{
	int a[] = { 6,3,2,1,5,7,9 };
	QuickSort(&a, 0, sizeof(a) / sizeof(int) - 1);
	Print(&a, sizeof(a) / sizeof(int));
}
int main()
{
	test();

	return 0;
}

4. Digging method

4.1 Implementation ideas

We specify to sort in ascending order , the name of the sorted array is a, the reference value key, left left, right right.

1. Select a key. The key can be any element in the array that needs to be sorted. We still choose the key as a[left], and give a[left] to the key to save. The position of the element selected as the key is the pit position ;

2. Go left from the right end (tail) of the array, when a[right] < key, right stops, put a[right] in the pit, and the pit is replaced by the subscript right, at this time right Do not move;

3. Then go to the right from the left end (head) of the array, when a[left] > key, left stops, put a[left] in the pit, and the pit is changed to the position with the subscript left, at this time left does not move;

4. Repeat steps 2 and 3. When the left and right go to the same position, this position is the last pit position. Put the key in this pit position, and the final position of the key is determined, and there is no need to adjust it later .

5. At this time, the key divides the array into left and right intervals. We use the first 4 steps for the left and right intervals, and then determine the keys of the left and right intervals again, recurse the left interval, and then recurse the right interval to achieve sorting.

The difference between the digging method and the hoare version:

1. The pit digging method does not need to think that the elements at the final meeting position of L and R are smaller than the key, because the final meeting position is the pit position, just put the key directly into the pit position;

2. Don’t have to think about why you should go to the right first if you choose the left as the key, and go to the left first if you choose the right as the key. When you choose the left as the key, because you need to fill in the hole on the left, you must go to the right first. It is more appropriate to understand this sentence by looking for the small on the left and the large on the right.

4.2 Diagram of thinking

4.3 Code implementation of digging method

// 快速排序挖坑法
int PartSort2(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 QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int keyi = PartSort2(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

4.4 Digging method code test

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
void Print(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}
// 快速排序挖坑法
int PartSort2(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 QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int keyi = PartSort2(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}
void test()
{
	int a[] = { 6,3,2,1,5,7,9 };
	QuickSort(&a, 0, sizeof(a) / sizeof(int) - 1);
	Print(&a, sizeof(a) / sizeof(int));
}
int main()
{
	test();

	return 0;
}

5. Front and rear pointer versions

5.1 Implementation ideas

We specify to sort in ascending order , the name of the sorted array is a, and the reference value is key .

1. Select a key, the key can be any element in the array to be sorted, we still choose the key as a[left];

2. Define a prev pointer and a cur pointer, initialize prev to point to the head position of the array, and cur point to the next position of prev. Cur goes first, cur finds elements smaller than key, stops after finding them, lets prev++, and then swaps (a[cur], a[prev]). After the exchange, continue to go back, the value that cur finds is not less than the key, cur continues to go back, after finding it, let prev++, exchange (a[cur], a[prev]), and repeat this step;

3. When cur walks through the entire array, exchange (a[left], a[prev]), and then the final position of the key is determined. key divides the array into left and right subintervals, the left subinterval is smaller than the key, and the right subinterval is larger than the key;

4. The left and right sub-intervals continue to repeat the first 3 steps, and the sorting of the array is realized by recursively going down.

5.2 Diagram of thinking

The continuous exchange here is actually to keep throwing the value smaller than the key forward, and throwing the value larger than the key backward. The value between cur and prev is actually all the values ​​larger than the key. The continuous exchange realizes the final division by key The left and right intervals, the left interval is smaller than the key, and the right interval is greater than the key.

5.3 Code implementation of front and back pointer method

// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
	int prev = left;
	int cur = left + 1;
	int keyi = left;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}

		cur++;
	}
	Swap(&a[keyi], &a[prev]);
	return prev;
}
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int keyi = PartSort3(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

5.4 Code test before and after the pointer method

// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
	int prev = left;
	int cur = left + 1;
	int keyi = left;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}

		cur++;
	}
	Swap(&a[keyi], &a[prev]);
	return prev;
}
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int keyi = PartSort3(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}
void test()
{
	int a[] = { 6,3,2,1,5,7,9 };
	QuickSort(&a, 0, sizeof(a) / sizeof(int) - 1);
	Print(&a, sizeof(a) / sizeof(int));
}
int main()
{
	test();

	return 0;
}

6. Time complexity analysis

6.1 Best case

In the above three cases, the best case time complexity is O(N*logN) .

Each time the key is placed in the middle of the interval, it needs to recurse logN times like a binary tree, and the time complexity of each subinterval sorting is O(N), so the best case is O(N * logN).

6.2 Worst case

When the array is sorted, the time complexity is O(N^2) regardless of whether the key is leftmost or rightmost .

7. Optimized quick sort

There are two ideas for quick sort optimization:

1. We can optimize the key selection method;

2. To recurse to a small subinterval, we can consider using insertion sort, also known as inter-small optimization.

7.1 Key selection optimization

Key selection optimization is mainly for arrays that are ordered, or close to ordered.

We have two ideas for optimizing key selection:

1. Randomly select the key;

2. Take the selected key for three numbers. (Take out left, mid, right, and select an intermediate value among the numbers whose subscripts are these three positions as the key).

The first way of thinking is uncontrollable, so the second way of selecting keys is the most appropriate.

The following is the optimized code for taking three numbers:

int GetMidIndex(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
			return mid;
		else if (a[left] < a[right])
			return right;
		else
			return left;
	}
	else //a[left] > a[mid]
	{
		if (a[mid] > a[right])
			return mid;
		else if (a[left] > a[right])
			return right;
		else
			return left;
	}
}
int PartSort3(int* a, int left, int right)
{
	int midi = GetMidIndex(a, left, right);
	Swap(&a[left], &a[midi]);

	int prev = left;
	int cur = left + 1;
	int keyi = left;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	Swap(&a[prev], &a[keyi]);
	keyi = prev;
	return keyi;
}

After we get it, we exchange the number with a[left], and it is no problem to still use the previous method of forward and backward pointers. The Hall version is the same optimization method as the digging method.

If we don't optimize the selection of three numbers, when the array is ordered or close to ordered, the time complexity will be the worst case, O(N^2). After taking the center of three numbers, if the array is ordered, the time complexity is still O(N * logN).

7.2 Inter-cell optimization

When recursing, it is not difficult to see in the picture we drew before. When we are constantly dividing, we will divide more and more later. When the amount of data is particularly large, the consumption of the stack will be very large, which will cause Risk of stack overflow. Therefore, when the division reaches a certain level, we no longer divide and directly choose insertion sort. Under normal circumstances, when the number of subinterval data is 10, we no longer recurse, and use insertion sorting directly.

Implementation code:

// 插入排序
//时间复杂度(最坏):O(N^2) -- 逆序
//时间复杂度(最好):O(N) -- 顺序
void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int tmp = a[i + 1];
		
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
}
int GetMidIndex(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
			return mid;
		else if (a[left] < a[right])
			return right;
		else
			return left;
	}
	else //a[left] > a[mid]
	{
		if (a[mid] > a[right])
			return mid;
		else if (a[left] > a[right])
			return right;
		else
			return left;
	}
}
// 快速排序前后指针法
//[left, right]
int PartSort3(int* a, int left, int right)
{
	int midi = GetMidIndex(a, left, right);
	Swap(&a[left], &a[midi]);

	int prev = left;
	int cur = left + 1;
	int keyi = left;
	while (cur <= right)
	{
		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 left, int right)
{
	//子区间只有一个值,或者子区间不存在的时候递归结束
	if (left >= right)
		return;

	//小区间优化
	if (right - left + 1 < 10)
	{
		InsertSort(a + left, right - left + 1);
	}

	int keyi = PartSort3(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

These two optimization methods have a certain degree of improvement in terms of time and space , but the essence of quick sorting has not changed, and optimization is just icing on the cake on the original idea.

Guess you like

Origin blog.csdn.net/Ljy_cx_21_4_3/article/details/131794152