插入排序,選擇,冒泡,快速排序算法及優化

一開始只想貼代碼,但是後來發現還是有人看的。所以覺得還是有必要花點時間去寫寫。

先定義一個用於交換兩個數的函數吧。

#if 0

inline void swap(int& a, int& b)
{
	int c = a;
	a = b;
	b = c;
}
#else
//
inline void swap(int& a, int& b)
{
	if (a != b)
	{
		a = a^b;
		b = a^b;
		a = a^b;
	}
}
#endif

用於獲得一段代碼執行時間的宏,限於windows系統,其他系統可以用相應的獲取時間的函數代替GetTickCount()。

/*
*獲取一段代碼(func)執行的時間
*/
#define GetTm(func, t)				\
do{									\
	DWORD t1, t2;					\
	t1 = GetTickCount();			\
	func;							\
	t2 = GetTickCount();			\
	t = (t2 - t1);					\
}while(0)

第一個排序算法,冒泡:

簡單的冒泡的過程像是在水裏有個氣泡,他一直往上冒一樣。

我就不多說了。直接看代碼:

/*
簡單冒泡排序
*/
void Buble(int a[], int n)
{
	assert(NULL != a && n > 1);
	for (int i = 0; i < n - 1; i++)
	{
		for (int j = 0; j < n-i-1; j++)
		{
			if (a[j] > a[j+1])
			{
				swap(a[j], a[j+1]);
			}
		}

	}
}

 直接來說優化。我們記錄下最後一次發生交換的地方,之後的都是有序的

/*
* 改進,記錄最後一次交換的位置,則這個位置之後的是有序的,無交換則全有序
*/
void Buble1(int a[], int n)
{
	assert(NULL != a && n > 1);
	int pos = 0;
	for (int i = n-1; i > 0; )
	{
		pos = 0;
		for (int j = 0; j < i ; j++)
		{
			if (a[j] > a[j + 1])
			{
				pos = j;//記錄最後一次交換的位置
				swap(a[j], a[j + 1]);
			}
		}
		i = pos;//之後的有序,不用再遍歷
	}
}

第二種優化,我們可以同時首尾都進行冒泡,一次找到一個最大的,一個最小的。可能我寫的代碼有問題。經過我多次試驗,效率反而下降。。或者數據很不湊巧。

/*
	首尾同時進行冒泡排序。
*/
void Buble2(int a[], int n)
{
	assert(NULL != a && n > 1);
	int high = n - 1;
	int low = 0;
	int i, j;
	while (low < high)
	{
		//找到最大的。
		for (j = low; j < high; j++)
		{
			if (a[j] > a[j+1])
			{
				swap(a[j], a[j+1]);
			}
		}
		high--;
		//找到最小的
		for ( i = high; i > low; i--)
		{
			if (a[i] < a[i-1])
			{
				swap(a[i], a[i-1]);
			}
		}
		low++;
	}
}

選擇排序:

選擇排序的思想,一次選出一個最大的數,或者找到最小的數。

代碼

/*
選擇排序
*/
void ChooseSort(int a[], int n)
{
	assert(NULL!=a && n>1);
	for (int i = 0; i < n-2; i++)
	{
		for (int j = i+1; j < n-1; j++)
		{
			if (a[i] > a[j])
			{
				swap(a[i], a[j]);
			}
		}
	}
}

優化的話,一次其實也可以找到一個最大值,一個最小值,我實際測試的時候,發現還是有很大提升的。
/*
選擇排序,每次找到最大和最小
*/
void ChooseSort1(int a[], int n)
{
	assert(NULL != a && n>1);
	int min = a[0], max = a[0];
	int minPos =0, maxPos = 0;
	int i, j;
	for ( i = 0; i < n-i-1; i++)
	{
		min = a[i];
		max = a[i];
		for ( j = i; j < n-i-1; j++)
		{
			if (min > a[j])
			{
				min = a[j];
				minPos = j;
			}
			if (max < a[j])
			{
				max = a[j];
				maxPos = j;
			}
		}
		swap(a[i], a[minPos]);
		swap(a[j], a[maxPos]);
	}
}

插入排序

數組前面的是有序的。後面的數,插入到前面有序部分相應的位置。

/*
*插入排序
*/
void InsertSort(int a[], int n)
{
	assert(NULL != a && n>1);
	int i, j,temp;
	for (i = 1; i < n; i++)
	{
		temp = a[i];
		for (j = i - 1; j >= 0 && temp < a[j]; j--)
		{
			a[j+1] = a[j];
		}
		a[++j] = temp;
	}
}

優化的話。。沒想到啥。。。


快速排序:

快速排序的思想是一次確定一個數的位置,使得他左邊比他小,右邊比他大。

/*
快速排序
*/
void QuikSort(int a[], int n)
{
	assert(NULL != a );
	int i = 0, j = n - 1;
	int temp = a[0];
	if (n <= 1)
	{
		return;
	}
	while (i < j)
	{
		while (i<j && temp <= a[j])
		{
			j--;
		}
		a[i] = a[j];
		while (i<j && temp >= a[i])
		{
			i++;
		}
		a[j] = a[i];
	}
	a[i] = temp;
	
	QuikSort(a, i );
	QuikSort(a + i + 1, n - i -1 );
}
對快排的優化,可以從下面的幾點出發,

1、與關鍵碼(樞軸)相同的數,可以移動到中間,下次不再參與遞歸。

2、樞軸的選擇很重要。如果每次都選擇第一個,則有可能出現一種情況,那就是已經是一個有序的數組的話,快排退化到冒泡。。。所以選取樞軸,儘量讓這個樞軸就是靠近數據中間位置的數,這個可以選擇數組兩端和中間的三個數做比較,選取第二大的(中間數)作爲樞軸,但是如果數組很大,這估計也不能得到比較好的效果,所以還有一種方法,選取前三個數,取中數,中間三個數,去中數,最後的三個數,取中數,然後在這三個中數中再選取中數,以最後選取出來的這個中數作爲樞軸。其實覺得還可以把數組分成三部分,0-1/4,1/4-3/4,3/4-1,主要是可以用位移運算才這麼分。然後分別取三個區間的中位數比較,最後再取三個中位數的中位數作爲樞軸。

例如 13,33,3,4,43,55,44,23,66,88,99

第一種選取13,55, 99,比較獲得樞軸爲55。

第一種選取13,33, 3,選取13。中間三個43,55,44選取44。 後面三個66,88,99,選取88,然後13,44,88再比較,最終選取44作爲樞軸。

第三種選取13,43,44,  66,99,最後比較取 44 這個中間數作爲樞軸。

3、當一次比較之後分出的子區間的個數很小。接近於8個左右,可以採用插入排序對這個子區間進行排序。

4、遞歸改爲循環,明顯第一個遞歸很好改,但是第二個遞歸就不那麼好改了。

基於以上說明。貼出代碼

//獲得三個數中第二大的數的位置
int GetMidNumPos(int arr[],int a, int b,int c)
{
	return (arr[a] < arr[b]) ? \
		((arr[b] < arr[c]) ? \
			b : ((arr[a] < arr[c]) ? c : a)) : \
		((arr[a] < arr[c]) ? \
			a : ((arr[b] < arr[c]) ? c : b));
}

void QuikSort1(int a[], int n)
{
    assert(NULL != a);

    if (n <= 1)
    {
        return;
    }
    int i, j, temp;
    if (n <= 7)
    {
        //個數小於7則插排
        for ( i = 1; i < n; i++)
        {
            temp = a[i];
            for ( j = i - 1; j >= 0 && temp < a[j]; j--)
            {
                a[j + 1] = a[j];
            }
            a[++j] = temp;
        }
        return;
    }
    else if ( n <= 40 )
    {
        //三數取中
        int mid = GetMidNumPos(a, 0, n >> 1, n - 1);
        temp = a[mid];
    }
    else
    {
        //9數取中
        int mid, mid1, mid2, mid3;
        mid1 = GetMidNumPos(a, 0, 1, 2);
        mid2 = GetMidNumPos(a, n >> 1 - 1, n >> 1, n >> 1 + 1);
        mid3 = GetMidNumPos(a, n - 3, n - 2, n-1);
        mid = GetMidNumPos(a, mid1,  mid2,  mid3);
        temp = a[mid];
    }

    int h, t;
    h =    i = 0;
    t = j = n - 1;

    while (true)
    {
        while (i<=j && temp >= a[i])
        {
            //先把與樞軸相同的元素移到左邊
            if (temp == a[i])
            {
                swap(a[i], a[h]);
                h++;
            }
            i++;
        }
        while (i<=j && temp <= a[j])
        {
            //先把與樞軸相同的元素移到右邊
            if (temp == a[j])
            {
                swap(a[j], a[t]);
                t--;
            }
            j--;
        }    
        if (i>j)
        {
            break;
        }
        swap(a[i], a[j]);
        i++;
        j--;
    }
    //把移動到兩邊與樞軸相同的數移動到中間。下次不再遞歸
    for (int k = 0; k < h; k++)
    {
        i--;
        swap(a[k], a[i]);
    }
    for (int k = n-1; k >t ; k--)
    {
        j++;
        swap(a[k], a[j]);
    }

    QuikSort1(a, i);
    QuikSort1(a + j + 1 , n - j - 1 );
}

做了一些改變之後的快排:

void QuikSort2(int arr[], int len)
{
	assert(NULL != arr);
	int n = len;
	int i, j, ti, tj, steps, temp;
	int* a = arr;
	int h, t, k;
	int mid, mid1, mid2, mid3;
	while (len > 1)
	{
		n = len;
		if (n <= 8)
		{
			for (i = 1; i < n; i++)
			{
				temp = a[i];
				for (j = i - 1; j >= 0 && temp < a[j]; j--)
				{
					a[j + 1] = a[j];
				}
				a[++j] = temp;
			}
			return;
		}
		else if (n <= 40)
		{
			mid = GetMidNumPos(a, 0, n >> 1, n - 1);
			temp = a[mid];
		}
		else
		{
			//數組分成三個區間,三個區間的中位數的中位數作爲樞軸
			int num1 = n >> 2;
			int num2 = n >> 1;
			mid1 = GetMidNumPos(a, 0, num1 >> 1, num1 - 1);
			mid2 = GetMidNumPos(a, num1, num2, num1 + num2);
			mid3 = GetMidNumPos(a, num1 + num2 + 1, num1 + num2 + num1>>1 + 1, n - 1);
			mid  = GetMidNumPos(a, mid1, mid2, mid3);
			temp = a[mid];
		}

		h = i = 0;
		t = j = n - 1;

		while (true)
		{
			while (i <= j && temp >= a[i])
			{
				if (temp == a[i])
				{
					swap(a[i], a[h]);
					h++;
				}
				i++;
			}
			while (i <= j && temp <= a[j])
			{
				if (temp == a[j])
				{
					swap(a[j], a[t]);
					t--;
				}
				j--;
			}
			if (i > j)
			{
				break;
			}
			swap(a[i], a[j]);
			i++;
			j--;
		}

		ti = i;
		//移動次數可以調整
		steps = (h < (ti - h)) ? h : (ti - h);
		for ( k = 0; k < steps; k++)
		{
			--ti;
			swap(a[k], a[ti]);
		}
		len = i - h + 1;

		tj = j;
		steps = (n - 1 - t) < (t - j) ? (n - 1 - t) : (t - j);
		for ( k = 0; k <steps; k++)
		{
			++tj;
			swap(a[n - 1 - k], a[tj]);
		}

		//第一層遞歸改用循環
		//	QuikSort2(a, i);
		//len = i;
		QuikSort2(a + j + 1, n - j - 1);
	}
}

經過我多次實踐,實踐上大部分時候第三種改過的反而沒有第二種快。。。我覺得主要原因可能是三目運算?:,還有取樞軸的時候計算多了一點。。


以上說明,代碼如有問題,請斧正,感激不盡。

謝謝。

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