1. 程式人生 > >【資料結構】常見的排序方法的實現以及效能對比

【資料結構】常見的排序方法的實現以及效能對比

前言:

  排序演算法在筆試和麵試的時候經常出現,有時候面試官可能不止問我們排序的寫法,還會問相關演算法的一些效能的比較。平時學習時,由於排序演算法較多,如果不做一些歸納總結的話,很容易混為一談,並且沒有對每個排序有很明確的定位。就會導致在面試的時候啞口。

一、插入排序

  在我看來插入排序的思路簡單,效率一般,最好的情況是O(N)即有序或接近有序的時候效率還是很高的,最壞的情況是O(N^2)即逆序時的情況,穩定。就效率而言算不上很優。下面看下插入排序的思路。

  1.思路分析

  插入排序指的是在陣列中取一個數,拿這個數跟前面的數比較,如果比前面的數小,那麼把當前的這個數往後挪一下,把後面的值挪到前面來,然後依次跟前面的依次比較。

//插入排序
void InsertSort(int* a, int size)
{
	for (int i = 0; i<size - 1; i++)
	{
		int end = i;
		int tmp = a[end + 1];
		while (end >= 0 && a[end] > a[end + 1])
		{
			a[end + 1] = a[end];
			a[end] = tmp;
			--end;
		}
	}
}

二、shell排序

  shell排序的最好情況為O(N)即有序的情況,最壞情況為O(N^2)即逆序的情況,但是shell排序的時間複雜度平均值為O(N^1.3)。

  1.思路分析

  其實shell排序差不多可以理解為插入排序的優化,每次調整序列使之接近有序然後做和插入排序相類似的操作,然後實現shell排序。

//希爾排序
void shellsort1(int a[], int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		for (int i = 0; i<n-gap; i++)//小於n-gap的沒一個數都比較一次
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0 && a[end]>a[end + gap])
			{
				a[end + gap] = a[end];
				a[end] = tmp;

				end = end - gap;
			}
			a[end + gap] = tmp;
		}
	}
}

三、選擇排序

  選擇排序的最好情況為O(N^2),最壞情況也為O(N^2),所以對於選擇排序是排序中比較差的那一種。

  1.思路分析

  實現思路就是每次選擇一個最小的放到前面,或者選擇一個最大的放到最後面(這裡是升序排序的),但是有優化就是選同時選一個最小最大的,放到最前和最後,這樣能夠提高一一倍的效率。

//選擇排序
void ChoiceSort(int* a,int size)
{
    assert(a);
    int min, max;
    for (int i = 0; i < size/2; i++)
    {
        min = i;
        max = size - 1 - i;
        for (int j = i+1; j < size; j++)//可能最小的一個值在最大值裡面
        {
            if (a[min] > a[j])
                min = j;

            if (a[max] < a[size-j-1])
                max = size-j-1;
        }
        if (min != i)
        {
            swap(a[i], a[min]);

            if (max == i)//如果max在第一個位置就會被換走,所以需要重新換回來,重定向到交換後的位置
            {
                max = min;
            }
        }
        if (max !=size-1-i)
        swap(a[max], a[size - 1 - i]);
    }
}

四、堆排序

  堆排序的最好情況是O(n*lgN),最壞也如此,因為堆排序是根據他的高度來決定的。

  1.思路分析

  堆排序的實現思路就是先建好一個大堆或者小堆,然後每次拿堆頭和最後一個葉子節點交換值,取出來以後放到最後,然後縮小區間調整堆。

void AdjustDown(int* a, int size, int parent)
{
	int child = parent * 2 + 1;
	while (child < size)
	{
		//找子節點中的最大值
		if (child + 1 < size&&a[child + 1] > a[child])
		{
			++child;
		}

		//如果孩子節點大於父節點交換
		if (a[parent] < a[child])
		{
			swap(a[parent], a[child]);
			parent = child;
			child = 2 * parent + 1;
		}
		else
		{
			break;
		}
	}
}

//堆排序
void HeapSort(int* a, int size)
{
	//建堆
	for (int i = (size-2)/2; i >= 0; i--)
	{
		AdjustDown(a, size, i);
	}

	//堆排序
	for (int i = 0; i < size; i++)
	{
		swap(a[0], a[size - 1 - i]);
		AdjustDown(a, size - 1 - i, 0);
	}
}

五、氣泡排序

  氣泡排序的最好情況是O(N),最壞是O(N^2),效率不是特別高

  1.思路分析

  氣泡排序的實現就是前後比較選出一個最大或最小的數放到最後,兩重迴圈控制就能實現了。但是有優化,只需要設定個標誌位判斷是否已經完成排序。

//優化的氣泡排序
void Bubble(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		bool judge = false;
		for (int j = 0; j < n - 1 - i; j++)
		{
			if (a[j] > a[j + 1])
			{
				swap(a[j], a[j + 1]);
				judge = true;
			}
		}
		if (!judge)
		{
			return;
		}
	}
}

六、快速排序

  快速排序就是個比較優的排序了,雖然他的最壞情況是O(N^2),但是他的總體情況是O(N*lgN)的,總體而言效率還是很高,再加上一些優化,效能還是很不錯的

  1.思路分析

  挖坑法:選定最右邊的值作為坑,從左邊開始找比選定的值大的,然後把值寫入這個位置,然後從右邊選一個小的,填入到剛剛轉移出去的位置,一直找直到左右指標相等,然後把選定的值填入中間位置就完成了。

  prev和cur法:prev一開始賦值為-1,cur賦值為0。cur遇到比選定的右邊的值大的話就往後走,遇到小的就停下來,然後++prev再交換prev和cur的值,使小的上前大的往後走,prev始終指向的是大的值的前一個。

  交換法:同樣選定右邊的值作為中間值,從左邊找一個大的停下來,從右邊找一個小的停下來,然後交換左右兩邊的值,這樣就實現了大的往後小的往前的目的。

//快速排序--》挖坑法
void QuickSort(int* a, int left,int right)
{
	if (left < right)
	{
		int low = left;
		int high = right;
		int key = a[low];
		while (low < high)
		{
			//從右邊找到第一個小於基數的位置
			while (low< high && a[high]> key)
				high--;
			a[low] = a[high];

			//從左邊找到第一個大於技術的位置
			while (low < high && a[low] < key)
				low++;
			a[high] = a[low];
		}
		a[low] = key;
		QuickSort(a, left, low - 1);
		QuickSort(a, low + 1, right);

	}

}
int PartSort(int* a, int left, int right)//prev和cur法
{
	assert(a);

	if (left < right)
	{
		int cur = left;
		int prev = left - 1;
		int key = a[right];
		while (cur < right)
		{
			if (a[cur] < key)
			{
				prev++;
				if (cur != prev)
					swap(a[cur], a[prev]);
			}
			cur++;
		}

		swap(a[++prev], a[right]);
		return prev;
	}
	return left;
}

void QuickSort_NonR(int* a, int left, int right)//非遞迴實現
{
	assert(a);
	stack<int> s;
	if (left < right)
	{
		//先壓入左,再壓入右,取的時候先取右,再取左,每次去左右區間排序下,然後一直迴圈下去,藉助棧,跟棧的原理差不多
		int mid = PartSort(a, left, right);
		if (left < (mid - 1))
		{
			s.push(left);
			s.push(mid - 1);
		}
		if (mid + 1 < right)
		{
			s.push(mid + 1);
			s.push(right);
		}

		while (!s.empty())
		{
			int high = s.top();
			s.pop();
			int low = s.top();
			s.pop();

			if (high - low < 13)
				InsertSort(a, high -low+1);

			mid = PartSort(a, low, high);

			if (low < mid - 1)
			{
				s.push(low);
				s.push(mid - 1);
			}

			if (mid + 1 < high)
			{
				s.push(mid + 1);
				s.push(high);
			}
		}
	}

}
七、歸併排序

  歸併排序的最好最壞情況都為O(N*lgN),穩定

  1.思路分析

  歸併排序的思路就是先開闢一塊空間,把原來的序列每次分一半,一直分到只有一個值為止,然後一個一個合併,先把排序好的值拷到開闢的空間中,然後再拷會原陣列,區間每次增加一半,直到增回原來的值。

void MergrPartSort(int* a, int first, int mid, int end, int* tmp)
{
	assert(a);
	int begin1 = first;
	int begin2 = mid + 1;
	int last1 = mid;
	int last2 = end;
	int i = first;

	while (begin1 <= last1 && begin2 <= last2)
	{
		if (a[begin1] <= a[begin2])
			tmp[i++] = a[begin1++];

		else
			tmp[i++] = a[begin2++];
	}

	while (begin1 <= last1)
		tmp[i++] = a[begin1++];

	while (begin2 <= last2)
		tmp[i++] = a[begin2++];

	for (int j = first; j <= end; j++)
	{
		a[j] = tmp[j];
	}

}

void MergrSort(int *a, int first, int end, int* tmp)
{
	assert(a);
	if (first < end)
	{
		int mid = first + (end - first) / 2;
		//一直遞迴到只有一個
		MergrSort(a, first, mid, tmp);
		MergrSort(a, mid + 1, end, tmp);
		//遞迴到最後一種情況的時候,做合併處理(相當於連結串列的合併)
		MergrPartSort(a, first, mid, end, tmp);
	}
}

void MergrSort_R(int* a,int n)
{
	assert(a);
	//開闢一塊空間,用於歸併排序時,將空間先在陣列中排序好,然後在放回原陣列
	int* tmp = new int[n];
	//如果開闢不成功,返回
	if (tmp == NULL)
		return ;

	MergrSort(a, 0,n-1,tmp);

	delete[] tmp;
}

//非遞迴寫法
void sort(int *a, int *tmp, int begin, int mid, int end){
	int begin1 = begin, begin2 = mid + 1, k=begin;
	while (begin1<=mid && begin2<=end)
	{
		if (a[begin1] <= a[begin2])
		{
			tmp[k++] = a[begin1++];
		}
		else
			tmp[k++] = a[begin2++];
	}

	while (begin1 <= mid)
	{
		tmp[k++] = a[begin1++];
	}
	
	while (begin2 <= end)
	{
		tmp[k++] = a[begin2++];
	}

	/*for (int i = begin; i <= end; i++)
	{
		a[i] = tmp[i];
	}*/
}



void Merge(int* a, int* tmp, int k, int n)
{
	int i = 0, j;
	while (i + 2*k <= n)//按照間隔為k,每一次分類2*k個區間,直到所有的都分類完
	{
		sort(a, tmp, i, i + k - 1, i + 2 * k - 1 );//為什麼中間值是i+k-1;因為這是做一個預設處理,將前面區間的最後一個值作為中間值,因為都是雙倍的
		i += 2 * k;
	}

	if (i+ k + 1 < n)//如果剩餘的個數比一個k長度還多,那麼就在進行一次合併
		sort(a,  tmp, i, i + k - 1, n - 1);

	else//其他情況說明這一塊是已經排序好了的,直接拷貝過去就行了,為什麼會排序好,因為前面排序小區間時排序好了
	{
		for (j = i; j < n; j++)
		{
			tmp[j] = a[j];
		}
	}
	for (i = 0; i < n; i++)//將值拷回原陣列
	{
		a[i] = tmp[i];
	}
}
//歸併排序的非遞迴寫法
void MergeSort_NonR(int* a, int n)
{
	int * tmp = new int[n];
	int i = 1;

	while (i < n)
	{
		Merge(a, tmp, i, n);
		i *= 2;//合併時每次翻一倍,1 2 4 8 16
	}

	delete[] tmp;
}

void testMerge_NonR()
{
	int a[] = { 5, 5, 3, 4, 7, 5, 2, 5, 1, 5, 0, 5, 5 };
	MergeSort_NonR(a, sizeof(a) / sizeof(a[0]));
	Print(a, sizeof(a) / sizeof(a[0]));
}