排序
文章目錄
最優解:先滿足時間複雜度最優,再滿足最小空間
穩定性:相同元素在排序時的先後位置不變
遞歸:在調用子過程的時候,會把父過程放入棧中,當子過程結束會回到父的棧中找到中斷行號,接着運行。
排序 | 時間複雜度 | 額外空間複雜度 | 穩定性 | 最好情況 | 最壞情況 |
---|---|---|---|---|---|
冒泡排序 | 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;
}
基數排序
補充:應用
歸併排序:最小和&逆序對
- 求小和: 算一個數左邊比它小的數的和,對數組中所有元素進行這個操作
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);
}
- 逆序對: 算數組中逆序對的數量
快速排序:荷蘭國旗問題
- 根據最後一個元素將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大小
- 全部完成後,將劃分值和最後一位交換位置
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穩定。