【數據結構】--常見的七大經典排序

引言

作爲數據結構又一大經典必考面試題,手撕排序算法題是很多人的噩夢,今天我們來梳理探索一下這七大經典算法。明其理,熟其用,窮其變。

排序

排序是計算機內經常進行的一種操作,其目的是將一組“無序”的記錄序列調整爲“有序”的記錄序列。分內部排序和外部排序,若整個排序過程不需要訪問外存便能完成,則稱此類排序問題爲內部排序。反之,若參加排序的記錄數量很大,整個序列的排序過程不可能在內存中完成,則稱此類排序問題爲外部排序。內部排序的過程是一個逐步擴大記錄的有序序列長度的過程。(來自百度百科)

穩定性劃分

所謂穩定性指的是兩個想等元素,在排序過程發生之後,兩者的先後位置沒有發生改變.
在這裏插入圖片描述

①直接插入排序

  • 思想:每次從無序表中取出第一個元素,把它插入到有序表的合適位置,使有序表仍然有序。 第一趟比較前兩個數,然後把第二個數按大小插入到有序表中;
    第二趟把第三個數據與前兩個數從後向前掃描,把第三個數按大小插入到有序表中;依次進行下去,進行了(n-1)趟掃描以後就完成了整個排序過程。
    直接插入排序是由兩層嵌套循環組成的。外層循環標識並決定待比較的數值。內層循環爲待比較數值確定其最終位置。直接插入排序是將待比較的數值與它的前一個數值進行比較,所以外層循環是從第二個數值開始的。當前一數值比待比較數值大的情況下繼續循環比較,直到找到比待比較數值小的並將待比較數值置入其後一位置,結束該次循環。

1.元素集合越接近有序,直接插入排序算法的時間效率越高
2. 時間複雜度:O(N^2)
3. 空間複雜度:O(1),它是一種穩定的排序算法
4. 穩定性:穩定

在這裏插入圖片描述

代碼如下

void InsertSort(int* a, int n)//直接插入排序
{
	for (size_t i = 0; i < n - 1; ++i)//讓一段無序的數組有序
	{
		int end = i;//從最開始進行比較
		int tmp = a[end + 1];
		while (end >= 0)
		{
			if (tmp < a[end])
			{
				a[end+1]=a[end];//插入到比他大和小的合適位置   //Swap(&a[end + 1], &a[end]);
				--end;				
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;///插入的位置爲-1時
	}
}

②希爾排序

  • 思想:希爾排序是直接插入排序的優化版本,先選定一個整數,把待排序數組中所有記錄分成個組,所有距離爲的記錄分在同一組內,並對每一組內的記錄進行排序。然後重複上述分組和排序的工作。當到達=1時,所有記錄在統一組內排好序。

1.時間複雜度:O(N1.3—N2)
2.穩定性:不穩定

在這裏插入圖片描述

代碼如下

void ShellSort(int* a, int n)//希爾排序
{
	int gap = n-1;
	while (gap > 1)
	{
		gap = gap / 3 + 1; //不斷地縮小區間值
		for (size_t i = 0; i < n - gap; ++i)//
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					 a[end + gap] = a[end] ;
					 end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}	
}

③選擇排序

  • 思想:第一次從待排序的數據元素中選出最小(或最大)的一個元素,存放在序列的起始位置,然後再從剩餘的未排序元素中尋找到最小(大)元素,然後放到已排序的序列的末尾。以此類推,直到全部待排序的數據元素的個數爲零。

1.直接選擇排序思考非常好理解,但是效率不是很好。實際中很少使用
2. 時間複雜度:O(N^2)
3. 空間複雜度:O(1)
4. 穩定性:不穩定

在這裏插入圖片描述

代碼如下

void AdjustDown(int *a, int size, int root) //向下調整算法,升序建大堆,降序建小堆
{
	int parent = root;
	int chrild = parent * 2 + 1;
	while (chrild < size)
	{
		if (chrild + 1 < size && a[chrild] < a[chrild + 1])
		{
			++chrild;
		}
		if (a[chrild] > a[parent])
		{
			Swap(&a[parent], &a[chrild]);
			parent = chrild;
			chrild = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void HeapSort(int *a, int n)
{
	for (int i = (n - 1 - 1) / 2; i >= 0;--i)
	{
         AdjustDown(a, n, i);
	}
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);//將最小的元素交換到最上邊
		AdjustDown(a, end, 0);
		--end;
	}
}

④堆排序

  • 思想:堆排序(英語:Heapsort)是指利用堆這種數據結構所設計的一種排序算法。堆是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。在堆的數據結構中,堆中的最大值總是位於根節點(在優先隊列中使用堆的話堆中的最小值位於根節點)。此處需要用到向下調整算法,需要注意的是排升序要建大堆,排降序小堆

1.堆排序使用堆來選數,效率就高了很多。
2. 時間複雜度:O(N*logN)
3. 空間複雜度:O(1)
4. 穩定性:不穩定

在這裏插入圖片描述

代碼如下

void AdjustDown(int* a, int size,int root)//向下調整算法,升序,建大堆(向下調整算法)
{
	int parent = root;
	int child = parent * 2 + 1;
	while (child < size)
	{
		if (child + 1 <size && a[child + 1] < a[child])
		{
			++child;
		}
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}		
	}
}
void HeapSort(int* a, int n)//堆排
{
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)//建堆
	{
		AdjustDown(a, n, i);
	}
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0],&a[end]);//將最小的數換到最頂部
		AdjustDown(a, end, 0);
		end--;
	}
}

⑤冒泡排序

  • 思想:冒泡排序是一種較爲簡單的排序,因爲他的簡單粗暴導致它的算法特性差。以升序爲例,就是重複遍歷數據,比較前後兩個元素的大小,把兩者大的一個交換到後面,直到把最大的元素調到最後邊,最小的調到最前邊爲止。

時間複雜度:T(n)=O(n²)
空間複雜度:S(n)=O(1)
穩定性: 穩定排序

代碼如下

void BubbleSort(int* a, int n)//冒泡排序(升序)
{
	int end = n - 1;
	while (end>=0)
	{
		int flag = 0;
		for (int i = 0; i < end; ++i)
		{		
			if (a[i]>a[i + 1])
			{
				flag = 1;
				Swap(&a[i], &a[i + 1]);
			}
		}
		if (flag == 0)//若循環走完flag爲0,則表示該數列有序
		{
			break;
		}
		--end;
	}
}

⑥快速排序

  • 思想:(1)首先設定一個分界值,通過該分界值將數組分成左右兩部分。 (2)將大於或等於分界值的數據集中到數組右邊,小於分界值的數據集中到數組的左邊。此時,左邊部分中各元素都小於或等於分界值,而右邊部分中各元素都大於或等於分界值。
    (3)然後,左邊和右邊的數據可以獨立排序。對於左側的數組數據,又可以取一個分界值,將該部分數據分成左右兩部分,同樣在左邊放置較小值,右邊放置較大值。右側的數組數據也可以做類似處理。
    (4)重複上述過程,可以看出,這是一個遞歸定義。通過遞歸將左側部分排好序後,再遞歸排好右側部分的順序。當左、右兩個部分各數據排序完成後,整個數組的排序也就完成了。

1 快速排序整體的綜合性能和使用場景都是比較好的,所以纔敢叫快速排序
2. 時間複雜度:O(N*logN)
3. 空間複雜度:O(logN)
4. 穩定性:不穩定

三種改進版本

  • hoare版本
  • 挖坑法
  • 前後指針版本

在這裏插入圖片描述

三種快排代碼如下

int GetMid(int* a, int left, int right)//(三數取中)
{
	assert(a);
	int mid = left + (right - left) / 2;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left]>a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[left]<a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

int PartSort1(int* a, int left,int right)//三數取中法
{
	int mid = GetMid(a, left, right);
	Swap(&a[mid], &a[right]);
	int key =right;
	while (left < right)
	{
		while (left < right && a[left] <= a[key])//左邊找比key大的數
			++left;

		while (left < right && a[right]>=a[key])//右邊找比key小的數
			--right;

		Swap(&a[left], &a[right]);
	}
	Swap(&a[key], &a[left]);//當left和right相遇時,交換key和left
	return left;
}

void QuickSort1(int* a, int left, int right)
{
	if (right <= left)
		return;
	int index = PartSort1(a, left, right);//遞歸思想
	QuickSort1(a, left, index - 1);
	QuickSort1(a, index + 1, right);

}


int PartSort2(int* a, int left, int right)//挖坑法
{
	int key = a[right];//用key記住這個位置
	while (left < right)
	{
		while (left < right && a[left] <= key)//左邊找比key大的
			++left;
		a[right] = a[left];//找到這個比key大的數據,填到key所在的位置,空出這個位置

		while(left < right && a[right] >= key)//右邊找比key小的
			--right;
		a[left] = a[right]; //找到這個比key小的數據,填到left所在的位置,空出這個位置
	}

	 a[left] = key;//當left和right相遇時,把key填到left位置
	return left;
}

void QuickSort2(int* a, int left, int right)
{
	if (right <= left)
		return;
	int index = PartSort2(a, left, right);//遞歸思想
	QuickSort2(a, left, index - 1);
	QuickSort2(a, index + 1, right);

}

int PartSort3(int* a, int left, int right)  //左右指針法
{
	int cur = left;
	int prev = left - 1;
	int key = a[right];
	while (cur< right)
	{
		if (a[cur] < key  && ++prev != cur)
		{
			
			Swap(&a[prev], &a[cur]);
		}
		++cur;
	}

	++prev;
	Swap(&a[prev],&a[right]);

	return prev;
}

void QuickSort3(int* a, int left, int right)
{
	if (right <= left)
		return;
	int index = PartSort3(a, left, right);//遞歸思想
	QuickSort3(a, left, index - 1);
	QuickSort3(a, index + 1, right);
}

⑦歸併排序

  • 思想:歸併排序(MERGE-SORT)是建立在歸併操作上的一種有效的排序算法,該算法是採用分治法(Divide andConquer)的一個非常典型的應用。將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱爲二路歸併。

1 歸併的缺點在於需要O(N)的空間複雜度,歸併排序的思考更多的是解決在磁盤中的外排序問題。
2. 時間複雜度:O(N*logN)
3. 空間複雜度:O(N)
4. 穩定性:穩定

在這裏插入圖片描述

代碼如下

void _MergeSort(int* a, int left, int right, int* tmp)//歸併排序
{
	if (left == right)
		return;
	int mid = left + (right - left) / 2;
	// [left, mid] [mid+1, right]分別有序
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid + 1, right, tmp);

	// a[left, mid] a[mid+1, right]歸併到tmp[left, right]
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	int i = left;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[i++] = a[begin1];
			++begin1;
		}
		else
		{
			tmp[i++] = a[begin2];
			++begin2;
		}
	}
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1];
		++begin1;
	}
	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2];
		++begin2;
	}
	memcpy(a + left, tmp + left, sizeof(int)*(i - left));
}
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int)* n);
	_MergeSort(a, 0, n - 1, tmp);
	free(tmp);
}

在這裏插入圖片描述

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