排序(數據結構)

比較排序

插入排序(斗地主摸牌就是一個插入排序)

單純的插入排序也叫直接插入排序

時間復雜度:

  • 最好O(n)
  • 最壞O(n^2)

過程
先寫單趟,再寫整體
依次比較,如果大于就往后挪動,否則就退出循環,插入數據

void InsertSort(int* a, int n)
{for (int i = 1; i < n; i++){int end = i-1;int tmp = a[i];//講tmp插入到[0,end]區間,保證有序//和前一次順序有關系 所以最好的時候就是O(n)while (end >= 0){if (a[end] > tmp){a[end + 1] = a[end];end--;}else{break;}}a[end + 1] = tmp;}
}

希爾排序 (分組插排)

希爾排序也是一個插入排序,不過他對原來的插入排序做了優化
時間復雜度

  • O(n^1.3)

過程
1.預排序 --目標 數組接近有序
2.直接插入排序

//希爾排序
//簡化寫法
//多組并排
//時間復雜度O(n^1.3)
void ShellSort(int* a, int n)
{int gap = n;//gap > 1while (gap > 1){//gap /= 2;gap = gap / 3 + 1;for (int i = 0; i < n - gap; i++){int end = i;int tmp = a[end + gap];while (end >= 0){if (a[end] > tmp){a[end + gap] = a[end];end -= gap;}else{break;}}a[end + gap] = tmp;}}
}

時間復雜度的分析

希爾排序是一個很復雜的排序,他的時間復雜度是一個增量式序列問題,如果我們只算邊界的話,它可以約等于n,但是他的中間應該是一個上升和下降的曲線。但是該怎么證明嘛,這里沒有明確的回答,涉及到了一些數學問題。正常分析可能會認為是n*logn,但是局部的結論認為他是n^1.3次方,這也是他復雜的原因,因為它沒有一個明確的證明過程,可以記一下結論。這里如果非要深究的話,和gap取值是有關的,關鍵是gap的取值不是定值,而是一直在變化。目前比較認可的取值是n/2和n/3+1。如果有興趣的話可以研究一下,但是這就涉及到一些數學領域的問題啦。

直接選擇排序

時間復雜度

  • O(n^2)

直接選擇排序很簡單,同時也是一個最爛的排序。
為什么說這是一個最爛的排序呢 因為他的時間復雜度無論是最好還是最壞的情況都是O(n^2),他的每一次排序都是從新選擇,和前面的排序沒有關聯。
選擇排序和插入排序的關系就像一個是斗地主的時候邊摸邊排序,一個是等發完所有的牌,再去給牌排序。

//選擇排序
//時間復雜O(n^2)
//最壞O(n^2)
//最好O(n^2)
//最垃圾的算法  他的前一次順序調整和下一次沒有關系 無論什么時候都是一個等差數列相加
void SelectSort(int* a, int n)
{int left = 0, right = n - 1;while (left < right){int min = left, max = left;//單躺for (int i = left + 1; i <= right; i++){if (a[i] > a[max]){max = i;}if (a[i] < a[min]){min = i;}}Swap(&a[left], &a[min]);//left 和 max重疊if (left == max){max = min;}Swap(&a[right], &a[max]);left++;right--;}
}

冒泡排序(默認升序)

時間復雜度

  • O(n^2)

冒泡排序屬于交換排序的一種
不做優化的情況下時間復雜度都是O(n^2),做了優化的情況下最好的情況是O(n)
兩兩交換,一次遍歷下來肯定能能把最大值排好,他和插入排序也是一個等差數列相加。
那冒泡排序和插入排序那一個更好呢?答案是插入排序。
雖然都屬于一個量級,但是在數據是部分有序和接近有序的時候插入排序會更快,比如數據1,3,2,4,5。
大家可能不理解,不是時間復雜度都一樣了嗎?為啥還有更優的說法呢?這里時間復雜只是一個量級標準,就像我們都是在校大學生,但是大學生就沒有區別嗎?有的人早起備考四六級,考研;有的人天天宿舍打游戲,通宵熬夜。出去講,我們都會說自己是大學生,這是一個量級標準,但是你們還是很有區別。如果你非要從時間復雜度為O(n^2)選擇一個排序,那插入排序絕對是YYDS。
冒泡排序其實更多的是教學意義,實際中不太會用他。

堆排序

時間復雜度

  • O(n*logn)

結論

  • 排升序,建大堆
  • 排降序,建小堆

這里排序建堆,是有點反正常的。這里采用的是,從下向上逐漸建堆,類似于后序遍歷。建好堆(升序為例),從堆頂取數據放到末尾,再對剩余的數組建堆(向下調整),重復操作。這里建堆,采用向上調整和向下調整都行,這里推薦使用向下調整(一個函數搞定)這里排序見大堆還是小堆,其實不是絕對,這里推薦的是最佳的解法,如果非要使用排升序,建小堆,也不是不能實現

//向下調整
void AdjustDown(int* a, int n, int parent)
{int child = parent * 2 + 1;while (child < n){//注意越界if (child + 1 < n && a[child + 1] > a[child]){child++;}if (a[child] > a[parent]){Swap(&a[child], &a[parent]);parent = child;child = parent * 2 + 1;}else{break;}}
}
//堆排序
//時間復雜度O(n*logn)
void HeapSort(int* a, int n)
{//模擬插入建堆//排升序 建大堆//排降序 減小堆//向下調整建堆 效率更高 一般使用向下調整建堆  o(n - log n)for (int i = (n - 1 - 1) / 2; i >= 0; i--){AdjustDown(a, n, i);}int end = n - 1;//這里的時間復雜度計算和向上建堆一樣 N*logNwhile (end > 0){Swap(&a[0], &a[end]);AdjustDown(a, end, 0);end--;}
}

快速排序

時間復雜度

  • O(n*logn)

1962年由Hoare提出的一種二叉樹結構的交換排序,它的基本思想是,選基準值/關鍵值,把他放在一個正確的位置(比他小的放在左邊,比它大的放在右邊),遞歸該過程。(升序為例)

結論:以左邊為基準值,先從右邊找,相遇位置一定是比key小的,以右邊為基準值,先從左邊找,相遇位置一定是比key大的。

這里分析是兩種情況,右邊先遇到左邊,左邊先遇到右邊。數據分析下就可以的出來,很簡單。
如果你是以左邊為基準值,先從左邊找,相遇位置一定是比key小的,
這個相遇位置不一定是比key小,這里隨便找一個反例,就可以得出來。

最原始的快排有個致命問題就是key值的選擇如果固定,在有序的時候,就是最壞的情況。遞歸太深了,會棧溢出。所以,這里提供了兩種解決方案,三數選中和隨機選key。這里更推薦三數選中,他完美解決了有序的情況。

這里有三種實現快速排序的方法,第一種就是最純粹的Hoare版本,但是Hoare版本不太好理解,容易寫錯。所以就有大佬進行改編,衍生出了挖坑法和前后指針法。這種方法只是對單趟的遍歷進行了改編,本質上還是快排的思想,還是遞歸,時間復雜度也是屬于O(n*logn)。這里推薦使用前后指針的方法,因為這個方法比較簡單而且不容易寫錯。
這里介紹的都是遞歸快排的寫法,但是遞歸就有一個致命問題,那就是遞歸太深會棧溢出,這其實不是代碼的問題,是編譯器的問題。所以,我們還要學習非遞歸的寫法。

三數選中

//選取中間的數
int GetMiduimNum(int* a,int left, int right)
{//int mid = left + right / 2; 錯誤寫法int mid =  left + (right - left) / 2;if (a[left] < a[mid]) // 2 < 3{if (a[right] > a[mid])//4 > 3{return mid; // 2 3 4}else if(a[left] > a[right]){return left;}else{return right;}}else //a[left] > a[mid]{if (a[mid] > a[right]){return mid;}else if (a[left] > a[right]){return right;}else{return left;}}
}

Hoare

快速排序 Hoare寫法 最簡單最原始版本
最壞情況O(n^2)
最好情況O(n*logn)
時間復雜度n*logn 加了優化就可以解決有序的情況
int Part1Sort(int* a, int left, int right)
{//結束條件if (left >= right){return;}int begin = left, end = right;//隨機選Key 版本//int randi = left + (rand() % (right - left));//Swap(&a[left], &a[randi]);//三數選中int mid = GetMiduimNum(a, left, right);Swap(&a[left], &a[mid]);int key = left;//left++ 這是一種錯誤寫法 千萬不要寫while (left < right){//右邊找小while (left < end && a[right] >= a[key]){right--;}//左邊找大while (left < right && a[left] <= a[key]){left++;}Swap(&a[left], &a[right]);}Swap(&a[key], &a[right]);key = left;return key;
}
//區間優化 減少遞歸次數 主要是遞歸深度 對性能影響不大
void QuickSort(int* a, int left, int right)
{//結束條件if (left >= right){return;}if (right - left + 1 > 10){int keyi = Part1Sort(a, left, right);//begin key -1 hole key+1 end//遞歸QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);}else{InsertSort(a + left, right - left + 1);}
}

挖坑法

//挖坑法 不再有左邊先走還是右邊先走 就是挖坑填坑 大思路不變
int Part2Sort(int* a, int left, int right)
{int begin = left, end = right;//三數選中int mid = GetMiduimNum(a, left, right);Swap(&a[left], &a[mid]);int hole = left;int key = a[left];while (left < right){while (left < end && a[right] >= key){right--;}a[hole] = a[right];hole = right;while (left < right && a[left] <= key){left++;}a[hole] = a[left];hole = left;}a[hole] = key;//回坑return hole;
}
//區間優化 減少遞歸次數 主要是遞歸深度 對性能影響不大
void QuickSort(int* a, int left, int right)
{//結束條件if (left >= right){return;}if (right - left + 1 > 10){int keyi = Part2Sort(a, left, right);//begin key -1 hole key+1 end//遞歸QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);}else{InsertSort(a + left, right - left + 1);}
}

前后指針

//前后指針
int Part3Sort(int* a, int left, int right)
{int begin = left, end = right;//三數選中int mid = GetMiduimNum(a, left, right);Swap(&a[left], &a[mid]);int prev = left, cur = left + 1;int keyi = left;while (cur <= right){if (a[cur] < a[keyi] && ++prev != cur){Swap(&a[cur], &a[prev]);}cur++;}Swap(&a[prev], &a[keyi]);keyi = prev;return keyi;
}
//區間優化 減少遞歸次數 主要是遞歸深度 對性能影響不大
void QuickSort(int* a, int left, int right)
{//結束條件if (left >= right){return;}if (right - left + 1 > 10){int keyi = Part3Sort(a, left, right);//begin key -1 hole key+1 end//遞歸QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);}else{InsertSort(a + left, right - left + 1);}
}

遞歸改非遞歸有兩種思路

  • 直接改循環 (斐波那契額數列)
  • 使用棧輔助(快排)

非遞歸寫法
棧里面取一段區間,單排分割子區間入棧,子區間只有一個值或者不存在就不入棧。這里其實就是在模擬遞歸而已,不理解的話,可以畫個遞歸展開圖。
快排其實還有一個非常致命的問題就是如果存在大量重復的問題,他的效率會變得很低,這個時候三數選中和隨機選key也不管用。這個時候需要一個三路劃分

//非遞歸寫法
void QuickSort(int* a, int left, int right)
{ST stack;STInit(&stack);STPush(&stack, right);STPush(&stack, left);while (!STEmpty(&stack)){int begin = STTop(&stack);STPop(&stack);int end = STTop(&stack);STPop(&stack);//先單趟int keyi = Part3Sort(a, begin, end);//begin keyi-1 keyi keyi+1 endif (keyi + 1 < end){STPush(&stack, end);STPush(&stack, keyi+1);}if (begin < keyi -1){STPush(&stack, keyi-1);STPush(&stack, begin);}}STDestroy(&stack);
}

總結

快排類似于一個二叉樹的前序遍歷,就像先找到根,在遍歷左子樹和右子樹。快排與普通的排序相比,時間復雜度已經有了質的提升。但是快排也不是一個簡單的排序。它有許多版本,最原始的就是Hoare版本,也是很難理解的。于是后面的挖坑法和前后指針法及誕生了。這里三種方法都要學習,但是平常寫建議前后指針,這是一種比較簡單的方法(如果你能理解)

歸并排序

歸并排序類似與一個二叉樹的后序遍歷,要對一段數字排序先找到這段數字的兩個有序子區間,使用比較大小,依次取數字,進行排序。找到兩個有序子區間就是一個分割歸并過程。

遞歸實現

void _MergeSort(int* a, int begin,int end,int* tmp)
{if (begin >= end){return;}int mid = (begin + end) / 2;//[begin mid] [mid+1 end]_MergeSort(a, begin, mid, tmp);_MergeSort(a, mid+1, end, tmp);//歸并int begin1 = begin, end1 = mid;int begin2 = mid + 1, end2 = end;int i = begin;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){tmp[i++] = a[begin1++];}else{tmp[i++] = a[begin2++];}}while (begin1 <= end1){tmp[i++] = a[begin1++];}while (begin2 <= end2){tmp[i++] = a[begin2++];}memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
//歸并排序
void MergeSort(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);_MergeSort(a, 0, n - 1,tmp);free(tmp);tmp = NULL;
}

非遞歸實現

//非遞歸 歸并排序
void _MergeSortNonR(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc failed");return;}int gap = 1;while (gap < n){for (int i = 0; i < n; i += 2*gap){int begin1 = i, end1 = i+gap-1;int begin2 = i+gap, end2 = i+2*gap-1;//printf("[%d %d][%d %d] ", begin1, end1, begin2, end2);int j = i;//修正路線/*	if (end1 >= n){end1 = end2 = n - 1;begin2 = n;}else if (begin2 >= n){end2 = n - 1;begin2 = n;}*/if (begin2 >= n){break;}else if (end2 >= n){end2 = n - 1;}printf("[%d %d][%d %d] ", begin1, end1, begin2, end2);while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){tmp[j++] = a[begin1++];}else{tmp[j++] = a[begin2++];}}while (begin1 <= end1){tmp[j++] = a[begin1++];}while (begin2 <= end2){tmp[j++] = a[begin2++];}//0 1 2 3memcpy(a+i, tmp+i, sizeof(int) * (end2-i+1));}//memcpy(a, tmp, sizeof(int) * (n));printf("\n");gap *= 2;}free(tmp);
}

非比較排序

計數排序

時間復雜度

  • O(n+range)

hash映射,記錄坑位數字出現的次數。當范圍很集中時,數據為int,比較適合。
局限性:對于double,字符串和很多其他情況無法完成。

void CountSort(int* a, int n)
{//找最大值和最小值int max = a[0], min = a[0];for (int i = 1; i < n; i++){if (a[i] > max){max = a[i];}if (a[i] < min){min = a[i];}}int range = max - min + 1;int* CountA = (int*)calloc(range, sizeof(int));//映射for (int i = 0; i < n; i++){CountA[a[i]-min]++;}//排序int j = 0;for (int i = 0; i < range; i++){while (CountA[i]--){a[j++] = i+min;}}
}

總結

對于非比較排序,其實還有很多,比如基數排序等等,但其實這些排序在實際中根本沒應用,而且很low,不僅局限而且效率也不咋樣。這里的計數排序,還有點運用,在某些情況他的時間復雜度是O(n),這已經吊打了快排這些排序。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/919793.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/919793.shtml
英文地址,請注明出處:http://en.pswp.cn/news/919793.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【C++組件】Elasticsearch 安裝及使用

&#x1f308; 個人主頁&#xff1a;Zfox_ &#x1f525; 系列專欄&#xff1a;C框架/庫 目錄&#x1f525; 介紹 &#x1f525; ES 安裝 &#x1f98b; 安裝 kibana&#x1f98b; ES 客戶端的安裝&#x1f525; ES 核心概念 &#x1f98b; 索引&#xff08;Index&#xff09;&…

項目:電動車報警器

1.項目需求 點擊遙控器A按鍵&#xff0c;系統進入警戒模式&#xff0c;一旦檢測到震動(小偷偷車)&#xff0c;則喇叭發出聲響報警&#xff0c;嚇退小偷。 點擊遙控器B按鍵&#xff0c;系統退出警戒模式&#xff0c;再怎么搖晃系統都不會報警&#xff0c;否則系統一直發出尖叫&a…

GDSFactory環境配置(PyCharm+Git+KLayout)

1、安裝 PyCharm 和 KLayout 安裝 PyCharm&#xff08;官網社區版即可&#xff09;和 KLayout&#xff08;官網最新版&#xff09;&#xff0c;這兩款軟件均開源&#xff0c;安裝操作簡單&#xff0c;這里不再贅述。&#xff08;注意&#xff1a;PyCharm軟件是否安裝成功以能否…

STM32 定時器(輸出模式)

?? ?一、輸出模式總覽?STM32定時器的輸出比較模式通過比較計數器&#xff08;CNT&#xff09;與捕獲/比較寄存器&#xff08;CCRx&#xff09;的值&#xff0c;控制輸出引腳&#xff08;OCx&#xff09;的電平狀態。六種模式定義如下&#xff1a;?模式宏??觸發動作?&am…

嵌入式硬件篇---手柄

手柄原理&#xff1a;手柄遙控的原理其實可以簡單理解為 “信號的發送與接收”&#xff0c;就像兩個人用對講機聊天&#xff0c;一方說話&#xff08;發送信號&#xff09;&#xff0c;另一方聽話&#xff08;接收信號&#xff09;&#xff0c;然后根據內容行動。下面用通俗的方…

數據庫架構開發知識庫體系

摘要面向初創與企業團隊&#xff0c;系統梳理數據庫與數據平臺從采集、傳輸、存儲、處理、服務化到治理與安全的全鏈路。覆蓋 OLTP/OLAP/HTAP、湖倉一體與實時數據棧&#xff0c;結合國內外工具與方法論&#xff0c;給出架構選型、性能優化、可靠性與合規要點&#xff0c;以及可…

在Excel和WPS表格中合并多個單元格這樣最快

如果要把WPS表格和Excel中多個單元格的數據合成到一個單元格中&#xff0c;不用函數&#xff0c;只需要先寫輸入公式&#xff0c;然后在各個單元格之間輸入&符號即可。&#xff08;當然&#xff0c;&符號不只是連接單元格的數據&#xff0c;也可以直接輸入內容連接&…

在嵌入式上用 C++14實現簡單HSM狀態機

文章目錄概述為什么要遷移到 C&#xff0c;以及 C 的陷阱目標與挑戰為什么不能直接使用 std::function&#xff1f;解決方案&#xff1a;POD 回調與模板 Trampoline核心設計模板 trampoline 實現兩種成員函數綁定策略1. **Per-Transition Context&#xff08;每個狀態轉移綁定一…

【unity】Obfuz加固混淆日志還原解析方案ObfuzResolver

Github | Gitee ObfuzResolver是基于obfuz-tools針對Obfuz的一項輔助工具&#xff0c;方便開發者在unity編輯器中或者運行時快捷將使用Obfuz混淆加固后的日志信息還原為原始信息&#xff0c;以輔助開發者快速定位Bug。 特性 支持unity編輯器模式下還原被加固混淆的日志信息&a…

2025DevOps平臺趨勢解讀:哪些DevOps工具正在引領行業變革?

DevOps平臺已成為企業提升研發效能、實現敏捷交付的核心支柱。2025年DevOps領域正經歷深刻變革&#xff0c;平臺能力正從“工具鏈整合”向“價值流智能中樞”躍升。01. 2025Devops平臺趨勢解讀“全棧式”與“模塊化/可組合”的平衡&#xff1a;企業既需要能覆蓋開發、測試、部署…

第二階段Winform-4:MDI窗口,布局控件,分頁

1_MDI窗口 &#xff08;1&#xff09;MDI是指將多控件窗體在同一窗體中打開,可以設置重疊打開&#xff0c;平捕打開等&#xff0c;MDI窗體&#xff08;Multiple-Document Interface&#xff0c;多文檔界面&#xff09;用于同時顯示多個文檔。在項目中使用MDI窗體時&#xff0c…

實用R語言機器學習指南:從數據預處理到模型實戰(附配套學習資源)

一、為什么需要掌握機器學習建模&#xff1f;在科研與項目實踐中&#xff0c;機器學習已成為數據挖掘的核心工具。本文手把手帶你在R語言中實現7大常用模型&#xff1a;邏輯回歸/正則化回歸決策樹/隨機森林SVM支持向量機XGBoost梯度提升神經網絡全程包含數據標準化→模型訓練→…

go.uber.org/zap 日志庫高性能寫入

使用 go.uber.org/zap 實現日志分割功能 實現按照單個文件最大MB自動分割,最多保留多少天的文件,是否啟用壓縮,按天自動分割日志 核心依賴 go.uber.org/zap:核心日志庫 lumberjack.v2:日志輪轉工具(實現按大小/時間分割) 時間處理依賴標準庫 time 實現步驟 1. 初始化…

電腦端完全免費的動態壁紙和屏保軟件(真正免費、無廣告、無會員)

? 1. Lively Wallpaper&#xff08;強烈推薦&#xff09; 特點&#xff1a;完全免費、開源、無廣告&#xff0c;支持本地視頻/GIF/網頁作為動態壁紙內置資源&#xff1a;12個高質量動態壁紙&#xff08;可自定義&#xff09;屏保功能&#xff1a;支持將動態壁紙一鍵設為屏保系…

C#_組合優于繼承的實際應用

2.2 Composition over Inheritance&#xff1a;組合優于繼承的實際應用 繼承&#xff08;Inheritance&#xff09;是面向對象編程中最容易被過度使用和誤用的特性之一。傳統的教學往往讓人們優先選擇繼承來實現代碼復用和建立“是一個&#xff08;is-a&#xff09;”的關系。然…

Kafka消息丟失的場景有哪些

生產者在生產過程中的消息丟失 broker在故障后的消息丟失 消費者在消費過程中的消息丟失ACK機制 ack有3個可選值&#xff0c;分別是1&#xff0c;0&#xff0c;-1。 ack0&#xff1a;生產者在生產過程中的消息丟失 簡單來說就是&#xff0c;producer發送一次就不再發送了&#…

盼之代售 231滑塊 csessionid 分析

聲明 本文章中所有內容僅供學習交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包內容、敏感網址、數據接口等均已做脫敏處理&#xff0c;嚴禁用于商業用途和非法用途&#xff0c;否則由此產生的一切后果均與作者無關&#xff01; 逆向分析 部分python代碼 url "…

STL關聯式容器解析:map與set詳解

目錄 1. 關聯式容器 2. 鍵值對 3. 樹形結構的關聯式容器 3.1 set 3.1.2 set的使用 3.2 map 3.2.1 map的介紹 3.2.2 map的使用 3.3 multiset 3.3.1 multiset的介紹 3.3.2 multiset的使用 3.4 multimap 3.4.1 multimap的介紹 3.4.2 multimap的使用 4.紅黑樹模擬實現…

貪吃蛇--C++實戰項目(零基礎)

視頻地址&#xff1a;C語言必學項目&#xff1a;貪吃蛇&#xff01; 貪吃蛇游戲框架 ├── 基礎框架 │ ├── 頭文件引入 │ ├── 常量和宏定義 │ └── 窗口初始化 │ ├── 數據結構系統 │ ├── Pos結構體(位置和顏色) │ ├── Snake結構體(蛇的屬性) │ ├──…

unity資源領取反作弊工具加密器

https://assetstore.unity.com/packages/tools/utilities/anti-cheat-pro-2025-3006260元購碼GUARDINGPEARSOFTWARE