c++排序: 冒泡、選擇、插入、歸併、快速、堆排序、桶排序

排序

github源碼地址


最優解:先滿足時間複雜度最優,再滿足最小空間

穩定性:相同元素在排序時的先後位置不變

遞歸:在調用子過程的時候,會把父過程放入中,當子過程結束會回到父的棧中找到中斷行號,接着運行。

排序 時間複雜度 額外空間複雜度 穩定性 最好情況 最壞情況
冒泡排序 N^2 1 可以穩定
選擇排序 N^2 1 不穩定
插入排序 N^2 1 可以穩定 N(已排序) N^2(逆序)
歸併排序 NlogN N 可以穩定
快速排序 NlogN logN 不穩定 每次劃分值剛好在中間區域 閾值左右兩側不平均
堆排序 NlogN 1 不穩定
桶排序 N N 穩定

一、冒泡排序

從小到大排序:從第一個數開始,兩兩比較,大的數往後放。

範圍從0N-1…0N-2…0N-3…01,0

穩定性:可以穩定,相等的時候讓後面的數做交換即可

void bubble_sort(vector<int> &a) {
	if (a.size() < 2) return;
	//每次找到的最小的元素被放到末尾
	for (int end = a.size() - 1; end > 0; --end)
	{
		for (int i = 0; i < end; i++)
		{
			if (a[i] < a[i + 1]) swap(a[i], a[i + 1]);
		}
	}
}

二、選擇排序

找到最小值的下標,和最後的元素交換

穩定性:不穩定,因爲需要最小值需要交換

void select_sort(vector<int> &a) {
	if (a.size() < 2)return;
	for (int end = a.size()-1; end > 0; --end)
	{
		int min = end;
		for (int j = 0; j < end; j++)
		{
			if (a[j] < a[min]) min = j;
		}
		swap(a[min], a[end]);
	}
}

三、插入排序

撲克牌插牌:新進來的牌和之前的元素依次比較確定插入的位置

穩定性:可以穩定,插入的時候只需放在相同的後面即可

void insert_sort(vector<int> &a) {
	if (a.size() < 2)return;
	for (int i = 1; i < a.size(); i++)
	{
		for (int j = i-1; j >=0; j--)
		{
			if (a[j] < a[j + 1])swap(a[j], a[j + 1]);
			else break;
		}
	}
}

更優質的寫法:

void insert_sort(vector<int> &a) {
	if (a.size() < 2)return;
	for (int i = 1; i < a.size(); i++)
	{
		for (int j = i-1; j >=0 && a[j]<a[j+1]; j--)
		{
			swap(a[j], a[j + 1]);
		}
	}
}

四、歸併排序:左排右排合併

遞歸,函數在調用的時候先分別排序自己的左右,然後再把左右合併起來

額外空間複雜度:O(N),需要額外的數組進行拷貝

穩定性:在merge的過程,只要小於等於就拷貝左邊部分

void merge(vector<int> &a, int left,int mid, int right) {
	if (left >= right || a.size() < 2) return;
	vector<int> temp;
	int l = left;
	int r = mid + 1;
	while (l<=mid && r <= right){ temp.push_back((a[l] < a[r] ? a[l++] : a[r++]));}
	while (l<=mid){ temp.push_back(a[l++]);}
	while (r<=right){ temp.push_back(a[r++]);}

	for (size_t i =0;i<temp.size();){
		a[left++] = temp[i++];
	}
	cout <<endl<< "temp: ";
	for (auto ax:temp)
	{
		cout << ax << " ";
	}
	cout << endl;
}

void mergesort(vector<int> &a,int left, int right) {
	if (left >= right || a.size() < 2) return;
	int mid = left + (right - left)/2;
	mergesort(a, left, mid);
	mergesort(a, mid + 1, right);
	merge(a, left, mid, right);
	cout << endl;
}

複雜度分析:

​ T(n) = T(n/2)+T(n/2)+O(n) = 2*T(n/2)+O(n)

用T(n) = a*T(n/b)+O(n^d)公式計算複雜度,

  • logba>d ,則算法複雜度爲 O(Nlogba)
  • logba<d,則算法複雜度爲 O(Nd)
  • logba=d,則算法複雜度爲 O(Nd*logN)

歸併排序:a=2,b=2,d=1,取相等的情況。

五、快速排序:分區遞歸

用荷蘭國旗問題的解決方法劃分區間,再對子問題進行遞歸調用。

時間複雜度討論:看遞歸調用的次數,如果數據按照1234567排放,需要不斷進行遞歸調用,效率很低。也就是當最右的劃分值左右兩側不平均的話代價就會很高。如果劃分值剛好能將左右區域劃分差不多大小,則每次大小減小爲原來的一半。

所以要選取優良的劃分值,最好用隨機選取的方法。隨機快速排序:將隨機選中的數和最後一個位置交換,再使用最後一個位置快速排序的方法,它的長期期望爲NlogN,並且常數項很低。工程的實際表現很好。

額外空間複雜度討論:劃分區間時需要記錄斷點值的信息,對於最好情況斷點空間爲logN,類似於二分。

穩定性:不穩定:因爲會交換。可以做到論文級別的,很難

// 以最後一個數爲劃分閾值
vector<int> partition(vector<int> &a,int l,int r) {
	int left = l-1;
	int right = r;
	while (l < right)
	{
		if (a[l] < a[r]) { swap(a[l], a[left + 1]); left++; l++; }
		else if (a[l] == a[r]) l++;
		else if (a[l] > a[r]) { swap(a[l], a[right - 1]); right--; };
	}
	swap(a[right], a[r]);
	return { left,right+1}; //返回小區間和大區間的下標
}

void quicksort(vector<int>&a, int left, int right) {
	if (a.size() < 2 || left >= right) return;
	vector<int> ind = partition(a, left, right);
	quicksort(a, left, ind[0]);
	quicksort(a, ind[1], right);
}

六、堆排序:堆頭交換下樹循環

堆:-----完全二叉樹結構:滿二叉樹或者通往滿二叉樹的路上,且都是從左到右排放的。要麼是滿的,要麼是未滿的層按左到右排放。

堆:----底層是數組。某節點下標: i;其子節點下標:左子 : 2 * i+1, 右子: 2 * i+2;其父節點下標:[(i-1)/2] 取整

大根堆:子樹的頭結點都是該子樹的最大值小根堆:子樹的頭結點都是該子樹的最小值

上樹

數組調成大根堆的過程

改成大根堆的實例:5 7 0 6 8,按照二叉樹的結構放入

  • 放入5
  • 放入7,7>父結點5,交換 7 5 0 6 8
  • 放入0,0<父結點7,不動
  • 放入6,6>父結點5,交換 7 6 0 5 8,6< 父結點7,不動
  • 放入8,8>父結點6,交換7 8 0 5 6,8>父結點7,交換 8 7 0 5 6
void bigRoot(vector<int> &a,int index) {//index用來確定數組尾端
	for (int i = 1; i <= index; i++)
	{
		if (a[i]>a[(i-1)/2])
		{
			int l = i;
			while(a[l]>a[(l-1)/2]){
				swap(a[l], a[(l - 1)/2]);
				l = (l-1)/2;
			}
		}
	}
}
下樹

把大根堆的根節點和最後一個位置交換,也就是把最大值放在數組尾端,

把剩下的數組調整成大根堆結構,也就是讓交換後的根節點下來

根節點和自己左右兩個孩子比較,與大於自己的孩子交換,不斷下樹

void downHill(vector<int> &a,int index) {
	int i = 0;
	while (2*i+1<=index)
	{
		int larger = ((2 * i + 2) <= index && (a[2 * i + 1] < a[2 * i + 2])) ? (2 * i + 2) : (2 * i + 1);
		if (a[i] <= a[larger]) {
			swap(a[i], a[larger]);
			i = larger;
		}
		else break;
	}
}
堆排序

堆排序的過程:

  • 上樹:建立大根堆,確定最大值,
  • 交換:將大根堆的根結點和末尾交換,把數組大小視作減小了1
  • 下樹:將交換後的根節點和左右子樹不斷比較,下樹
  • 交換…下樹…交換…下樹…
  • 排序結束
void heapSort(vector<int> &a) {
	if (a.size() < 2)return;
	int index = a.size() - 1;

	bigRoot(a, a.size() - 1);
	while (index>0)
	{
		swap(a[0], a[index--]);
		downHill(a, index);
	}
}

建立大根堆的過程複雜度爲log1+log2+…+logN = O(N)
調整所有數的過程爲O(NlogN)

缺點:不穩定,常數項大

七、桶排序

桶排序不是基於比較的排序。比如數字範圍從0~200,共幾億個數。設置201個桶,放桶裏,根據桶編號倒出到容器。如果容器不是棧,則桶排序穩定

計數排序
void buckersort(vector<int> &a) {
	if (a.size() < 2) return;
	int max = a[0];
	for (int i = 0; i < length; i++){
		max = a[i] > max ? a[i] : max;
	}
	vector<int> temp(max+1);
	for (int i = 0; i < length; i++){
		temp[a[i]]++;
	}
	for (int i = 0; i < max; i++){
		for (int j = 0; i < temp[i]; j++){
			temp.push_back(i);
		}
	}
	a = temp;
}
基數排序

補充:應用

歸併排序:最小和&逆序對

  1. 求小和: 算一個數左邊比它小的數的和,對數組中所有元素進行這個操作
int minsum(vector<int> &a) {
	int sum = 0;
	for (size_t i = 0; i < a.size(); i++){
		for (size_t j = 0; j < i; j++){
			if (a[i] > a[j]) sum += a[j];
		}
	}
	return sum;
}

int merge(vector<int> &a, int left,int mid, int right) {
	if (left >= right || a.size() < 2) return 0;
    vector<int> temp;
	int l = left;
	int r = mid + 1;
	int sum = 0;
	while (l<=mid && r <= right){ 
		sum += (a[l] < a[r]) ? a[l] * (right - r+1) : 0;
		temp.push_back((a[l] < a[r] ? a[l++] : a[r++]));
		
	}
	while (l<=mid){ temp.push_back(a[l++]);}
	while (r<=right){ temp.push_back(a[r++]);}

	for (size_t i =0;i<temp.size();){
		a[left++] = temp[i++];
	}
	cout <<endl<< "temp: ";
	for (auto ax:temp)
	{
		cout << ax << " ";
	}
	cout << endl;
	return sum;
}

int sort(vector<int> &a,int left, int right) {
	if (left >= right || a.size() < 2) return 0;
	int mid = left + (right - left)/2;
	return sort(a, left, mid) + sort(a, mid + 1, right)+merge(a, left, mid, right);
}
  1. 逆序對: 算數組中逆序對的數量

快速排序:荷蘭國旗問題

  1. 根據最後一個元素將vector分成兩個部分
// 以最後一個數爲劃分閾值
//小於等於(最後一個數)的區域,從左邊界開始
//返回元素被交換的位置
//當一個數小於等於p,就直接擴展小於等於區域的範圍
//當一個數大於p,將這個數交換到小於等於區的下一個位置,再擴充
int partition(vector<int> &a, int left, int right) {
	int temp = a[right];
	int l = left-1;
	for (int i = left; i < right;i++) {
		if(a[i] <= temp) 
			swap(a[++l],a[i]);
	}
	return l;
}
  1. 荷蘭國旗問題

選擇最後一個數爲劃分值,小於這個數的放左邊,等於放中間,大於放右邊。

做法:分兩塊區域:大於區和小於區,大於區以最後一個數爲起始點,小於區以-1位置爲起始點。

  • 當數等於劃分值,箭頭往下跳
  • 當數小於劃分值,小於區擴張1位
  • 當數大於劃分值,該數和大於區外的後一個數交換,大於區擴張1大小
  • 全部完成後,將劃分值和最後一位交換位置
void holland(vector<int> &a) {
	if (a.size() < 2) return;
	int left = -1;
	int cur = 0;
	int right = a.size() - 1;
	int last = a.size() - 1;
	int temp = a[last];
	while (cur<right)
	{
		if (a[cur] < temp) { swap(a[cur], a[left + 1]); left++; cur++; }
		else if (a[cur] == temp) cur++;
		else if (a[cur] > temp) { swap(a[cur], a[right - 1]); right--; };
	}
	swap(a[right], a[last]);
}

桶排序:計算數組排序後的相鄰兩數的最大差值

計算數組排序後的相鄰兩數的最大差值,要求時間複雜度O(N),也就是不允許用排序做。

思路:根據桶個數建立桶,範圍上劃分成n+1份。

比如:假設9個[0-99]的數,min=0, max = 99,準備9+1=10個桶,範圍分別爲[0-9],[10-19],[20-29]…[90-99],將9個數放入桶中。

邊界的桶由於min,max存在必不爲空,9數10桶,中間必有空桶。於是只需考慮空桶和其相鄰桶之間的關係,無需考慮桶內相鄰數,只需考慮桶內出現的最大值和最小值

A , B , _ , _ , C , D

first = min(B)-max(A)

second = min©-max(B)

third = min(D) - max©

return max(first,second,third)

int neighbormin(vector<int> a) {
	if (a.size() < 2)return 0;
	int min = a[0];
	int max = a[0];
	for (int i = 1; i < a.size(); i++)
	{
		if (a[i] > max) max = a[i];
		if (a[i] < min) min = a[i];
	}
	int bucketlen = (max - min) / (a.size() + 1) + 1;
	vector<int> bucketmin(a.size() + 1);
	vector<int> bucketmax(a.size() + 1);
	vector<bool> hasnum(a.size() + 1);

	for (int i = 0; i < a.size(); i++)
	{
		int bid = (a[i] - min) / bucketlen;
		bucketmax[bid] = hasnum[bid]? ((bucketmax[bid] <= a[i]) ? a[i] : bucketmax[bid]):a[i];
		bucketmin[bid] = hasnum[bid] ? ((bucketmin[bid] >= a[i]) ? a[i] : bucketmin[bid]) : a[i];
		hasnum[bid] = true;
	}

	int maxval = bucketmax[0];
	int res = 0;
	for (size_t i = 1; i < a.size() + 1; i++)
	{
		if (hasnum[i])
		{
			res = (bucketmin[i] - maxval) > res ? (bucketmin[i] - maxval):res;
			maxval = bucketmax[i];
		}
	}

	return res;
}

綜合排序

綜合排序分範圍

  • n < thred : insert
  • n > thred :merge(自定義類型)/quick(基礎類型)

理由:在數據量小的時候,常數量的優勢會體現,insert的常數量很低。

基礎類型的排序不關心穩定性,quick快

自定義類型的排序可能需要穩定性,比如數據庫排序中id和age,先按id排,再按age排就需要id穩定,merge穩定。

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