快速排序之代碼問題


實現快速排序之時,如果不注意細節,可能遺留一些漏洞。

無限循環

有一種實現看似能夠正常排序,其實存在無限循環的隱患。當左右遊標索引元素與中軸元素相等,由於無法進入內層循環,左右遊標無法移動,導致無限循環。代碼如下所示:

#include <stddef.h>

void qsort(int *base, int low, int high)
{
	if (base == NULL || low >= high \
		|| (low | high) <= 0)
		return;

	int mid = (low + high) / 2;
	int key = base[mid];
	base[mid] = base[low];
	base[low] = key;

	int left = low, right = high;
	while (left < right)
	{
		while (left < right && base[right] > key)
			--right;
		base[left] = base[right];

		while (left < right && base[left] < key)
			++left;
		base[right] = base[left];
	}
	base[left] = key;

	qsort(base, low, left - 1);
	qsort(base, left + 1, high);
}

對於此問題,最簡單的驗證方法是採用相等的元素測試。
而解決方案是增加等於判斷,在比較元素大小之時,考慮兩個元素相等的情況。代碼如下所示:

#include <stddef.h>

void qsort(int *base, int low, int high)
{
	if (base == NULL || low >= high \
		|| (low | high) <= 0)
		return;

	int mid = (low + high) / 2;
	int key = base[mid];
	base[mid] = base[low];
	base[low] = key;

	int left = low, right = high;
	while (left < right)
	{
		while (left < right && base[right] >= key)
			--right;
		base[left] = base[right];

		while (left < right && base[left] <= key)
			++left;
		base[right] = base[left];
	}
	base[left] = key;

	qsort(base, low, left - 1);
	qsort(base, left + 1, high);
}

判斷等於雖然可以解決無限循環問題,但是會增加最壞情況出現的概率。

快速排序的空間複雜度主要是遞歸佔用的棧空間。
最好情況,時間複雜度爲O(nlog2nnlog_2n),遞歸樹深度爲log2nlog_2n,空間複雜度爲O(log2nlog_2n)。
最壞情況,時間複雜度爲O(n2n^{2}/2),遞歸樹深度爲n-1,空間複雜度爲O(n)。

錯誤賦值

也有一種實現在比較元素之時可以不加等於判斷,並且防止無限循環。代碼如下所示:

#include <stddef.h>

void qsort(int *base, int low, int high)
{
	if (base == NULL || low >= high \
		|| (low | high) <= 0)
		return;

	int mid = (low + high) / 2;
	int key = base[mid];
	base[mid] = base[low];
	base[low] = key;

	int left = low, right = high;
	while (left < right)
	{
		while (left < right && base[right] > key)
			--right;
		base[left++] = base[right];

		while (left < right && base[left] < key)
			++left;
		base[right--] = base[left];
	}
	base[left] = key;

	qsort(base, low, left - 1);
	qsort(base, left + 1, high);
}

對於此實現代碼而言,其實無論是否判斷等於,都可能出現結果錯亂的現象。
以下列用例測試:
5 9 1 8 7 3 5 2 3 5
排序結果如下所示:
1 2 3 3 5 5 5 7 9 8
當然不僅上述用例出現錯誤賦值,其他情況也可能出現,究其起因,乃左遊標大於右遊標之時賦值。

賦值之前判斷左右遊標可以防止左遊標大於右遊標之時賦值,代碼如下所示:

#include <stddef.h>

void qsort(int *base, int low, int high)
{
	if (base == NULL || low >= high \
		|| (low | high) <= 0)
		return;

	int mid = (low + high) / 2;
	int key = base[mid];
	base[mid] = base[low];
	base[low] = key;

	int left = low, right = high;
	while (left < right)
	{
		while (left < right && base[right] > key)
			--right;
		if (left < right)
			base[left++] = base[right];

		while (left < right && base[left] < key)
			++left;
		if (left < right)
			base[right--] = base[left];
	}
	base[left] = key;

	qsort(base, low, left - 1);
	qsort(base, left + 1, high);
}

不同形式

快速排序有多種實現形式,上述無限循環和錯誤賦值兩個問題提出兩種實現形式。另外還有更簡潔的實現形式,如下所示:

#include <stddef.h>
#include <stdbool.h>

static inline void swap(int *left, int *right)
{
	if (left == right)
		return;

	int temp = *left;
	*left = *right;
	*right = temp;
}

void qsort(int *base, int low, int high)
{
	if (base == NULL || low >= high \
		|| (low | high) <= 0)
		return;

	int mid = (low + high) / 2;
	int key = base[mid];
	base[mid] = base[low];
	base[low] = key;

	int left = low, right = high + 1;
	while (true)
	{
		while (left < high && base[++left] < key);
		while (base[--right] > key);
		if (left >= right) break;
		swap(base + left, base + right);
	}
	swap(base + low, base + right);

	qsort(base, low, right - 1);
	qsort(base, right + 1, high);
}

此種實現的原理是通過左右遊標,分別尋找不小於和不大於中軸元素的兩個元素,並且交換兩個元素的位置,直到左右遊標重疊,讓中軸元素歸位。

最大容量

仔細觀察以上代碼,可以發現都以有符號整型作爲數量和下標類型,並且間接依賴於-1,卻浪費接近一半的元素數量。
雖然實際應用沒必要針對如此大量的元素採用內部排序,而且時間成本和空間成本都非常大,但是數量和下標都是自然數,採用無符號整型可以增加元素容量,讓功能變得更加強大,也避免殘留範圍隱患。

打開以下鏈接文件,查看針對C語言封裝的泛型快速排序:
https://gitee.com/solifree/pure-c/blob/master/算法/排序/quick_sort.c
此泛型快速排序依賴於下述兩個文件:
https://gitee.com/solifree/pure-c/blob/master/算法/排序/gtsort.h
https://gitee.com/solifree/pure-c/blob/master/算法/排序/swap.c
無符號整型的兩個極端值得注意,零減一爲最大值,最大值加一爲零,留意下標的比較和運算,代碼形式有所不同。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章