大家好呀,今天是中秋佳節,大家都回家和家人團聚了嗎?
今年的月餅好吃嗎?
不管怎么樣,博主都在這里祝大家中秋節快樂~~
今天我們就來寫一個功能強大的通用型別排序函式吧!
【手把手教你實作】通用排序函式
- 原型
- 思想
- 代碼實作
- 1、確定冒泡排序的趟數
- 2.確定一趟冒泡排序中兩兩比較的次數
- 3. 兩兩比較
- 4. 交換
- 總結
每次我們寫一個不同型別的排序的時候,我們都要寫重新寫一個新的排序函式,那么有沒有什么方法,可以寫一個適用于所有型別的排序函式呢?
當然是有的!接下來我們就一起來看看吧~
原型
首先我們知道庫函式中有一個qsort函式,它其實就適用于任何型別的資料,
(不要問我為什么已經有了qsort函式還非要自己寫一個,我知道你們都跟我一樣熱愛學習,必須要自己寫出來才肯罷休!)

所以,作為我們的通用排序函式的原型,我們首先要搞懂別人(qsort)是怎么做到“通用”的,只用把別人的工具弄明白了,我們才能寫出屬于自己的工具,
首先我們再cplusplus網站中看看qsort這個函式的真容,

首先我們看到第一個引數是一個指標,指標的型別是void*,這表示可以接收任意型別的指標,包括整型指標、字符指標、浮點型指標、結構體指標等等,
這樣,我們在傳參的時候就可以把任意型別的陣列作為引數傳給qsort,達到一個通用的方式,
后面的引數分別是size_t num、size_t size和
int (compar)(const void,const void*)),它們分別是什么意思呢?我們可以帶著疑問和猜測繼續往下看,

看到這里,我們應該大概知道,這個函式可以實作通用排序的原理,
我們先接收一個指向被排序的陣列的指標,然后告訴函式我們要排序的資料的數量以及每個元素的大小,最后再告訴函式這些資料之間應該怎么排序,然后剩下的交給函式按照我們的規則和要求去執行就可以了,
那么下面我們就先用qsort函式來實作排序,
首先我們把一個int陣列arr由降序排為升序,

那么除了給整型陣列、字符陣列等等我們常見的陣列之外,我們嘗試著用qsort給結構體進行排序,
首先我們創建一個結構體,在這個結構體型別之上創建相應的變數并初始化,

注意:上圖中為了方便,我直接把struct student型別重定義為s,
下面如果我們想要按照名字對arrs進行排序:

或者按照年齡排序進行排序:

其實也可以按照id排序,這里我就不寫了,大家可以自行實作哦(其實陣列中已經排成升序了,大家可以把它排成降序),
思想
從原型qsort中,我們知道了實作一個通用排序函式的原理,但是函式內部究竟應該如何實作排序的呢?
我們可以利用之前我們學過的一個排序思想:冒泡排序,來實作排序函式內部的排序方法,
詳見這篇文章:【手把手帶你入門】陣列的創建與使用(超詳細解釋,包括陣列傳參以及陣列名的理解)

我們知道,冒泡排序的思想是每一次讓相鄰兩個元素進行比較,一趟冒泡排序可以讓一個元素來到它應在的位置上,
我們還是先來看看我們之前用來實作的冒泡排序函式,

我們可以看到,通過冒泡排序實作排序的重點主要有4點:
- 確定冒泡排序的趟數:被排序元素的個數-1
- 確定一趟冒泡排序中兩兩比較的次數:剩余要排序的元素個數-1
- 兩兩比較
- 交換
明確了思想,我們就可以寫代碼啦!
代碼實作
首先我們一局qsort函式寫出my_sort函式的使用,

再依此(或者qsort函式)寫出my_sort函式的封裝,

1、確定冒泡排序的趟數

2.確定一趟冒泡排序中兩兩比較的次數

3. 兩兩比較
這里的兩兩比較就要根據不同型別用不同的比較方法,所以我們要用到之前傳給我們的比較函式,
按照排成升序的原則,如果比較函式回傳值<=0,則不進行交換;如果>0,則執行兩個元素的交換,

這里要注意,我們為什么把void*強制轉換成字符指標型別而不是整型指標型別等等?
因為,我們傳給函式的指標有可能是各個型別的指標,而不同型別的指標依次能跳過的記憶體步長是不一樣的,為了能夠遍歷不同型別元素中的每個位元組,我們應該用步長最小的字符指標型別來對元素進行小步多次的訪問,
這就好像我們從家里去學校選擇交通工具,我們可以選擇步行、公交、或者打車,打車雖然很方便,上車之后下一站就到目的地了,但是一路上我們就不能停下來;如果選擇公交,那么我們到一站就會停下來一次,這樣在路途中能看到的風景就更多更詳細;而如果選擇走路,那么我們想在哪里停下來就在哪里停下來,這樣我們就能完整地看到整個從家里到學校的所有路上的風景,
所以,這里(包括后面的交換)為了能夠完整地訪問元素中的內容,我們采用的是最小步長的char*指標,以防“步子”走太快,錯過了一些重要的“風景”,
4. 交換

因為每次都是交換一個位元組的大小,所以我們要根據一個元素包含的位元組數來確定一次元素的交換需要交換幾個位元組,


這樣,一個通用排序函式就寫完啦!
接下來,我們來驗證一下它的可行性吧!

在監視視窗中,可以看到arr陣列從降序被排成了升序,
雖然已經初步證明了排序函式的實作,但是我們還是再次用上面寫過的結構體來驗證一下吧!


從除錯的結果來看,我們的通用排序函式已經很好地實作啦!!!太棒啦~~~~
你學廢了嗎?

但是,等一等,回顧我們的代碼,我們還是可以再完善一下我們的代碼的!

我們發現,最初傳參的時候,num和size的型別都是size_t,其實就是unsigned int,所以我們后面在進行運算(比較)的時候,要注意變數之間型別的同一,避免編譯器警告噢~
所以這里,博主對代碼進行了修改,把代碼中的i、j等變數改成了size_t型別,

大家也要注意這些小細節噢!
雖然它們并不起眼,但是有時候很可能是決定問題的關鍵呢!
同一的代碼不僅能減少錯誤的發生,代碼讀起來也會更好,所以,我們都要不怕麻煩,向著完美進發噢!!!

總結
其實這個通用冒泡排序看似很復雜,但是寫下來好像也并不是很難呢!當然qsort函式內部并不一定是以冒泡排序的演算法來對元素進行排列的,當然感興趣的朋友們也可以去研究一下!
以下是博主寫完通用排序函式之后的一點總結和感悟:
-
我們的通用排序函式主要指的是型別通用,那么實作型別通用的一個關鍵因素在于比較函式,這里就依賴于回呼函式的存在,
我們不是自己直接去呼叫比較函式,而是通過這個通用排序函式來呼叫它,告訴它有這個函式,讓它自己去呼叫, -
我們可以發現,指標在函式傳參中的應用非常普遍,有了各種型別的指標之后,我們就可以通過傳參把變的地方告訴函式,不變的地方讓函式來幫助我們完成,這樣在寫一些明顯有重復的地方的時候,我們就可以用函式來替代那么重復,
-
思考通用排序函式完成的程序中,我們可以得到一種學習方法:先找到一個現有的東西,弄懂它的內涵,然后模仿它,在模仿的程序中,結合我們已經學過的內容,來實作一個自己的東西,我覺得這是一個非常好的學習和創造方法,不僅適用于代碼,更可用于我們生活學習中的方方面面,必須mark下來,
-
其實博主在寫代碼的程序中,并不是一帆風順,很容易在小地方出錯,然后找bug找了好久好久,最后發現是在一個很小很小的地方不小心把j寫成了i,所以寫代碼的時候一定要十分細心,遇到問題的時候,一定耐心一個字母一個字母地看,如果有錯誤提示,要根據錯誤提示思考可能出錯的地方,這樣有方向地找bug,效率會高很多,
-
最后,寫代碼不是一個一蹴而就的事情,我們寫完代碼之后,要回過頭來再驗證一下,再看看還有哪里可以更完美~每一次的更改會給下一次代碼更多減少錯誤和麻煩的經驗,
最最最最后,感謝友友們那么耐心地看到這里,如果你覺得我的文章對你有用,記得點贊收藏關注一波,在評論區留下你的腳印噢~

關注我,一起精進C語言吧~
本文代碼已經上傳到gitee啦~大家按需自取哦!
https://gitee.com/fang-qiuhui/my-code/blob/0167e797c445f809c626cfce2ead57fa31d69392/blog_2021_9_20_qsort/blog_2021_9_20_qsort.c
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/301997.html
標籤:其他
