目錄:java
前言程序員
一 冒泡排序面試
二 選擇排序算法
三 插入排序shell
四 希爾排序編程
五 歸併排序api
六 快速排序數組
七堆排序bash
另外這幾天沒事在刷算法題,感興趣的能夠看看:
這是在掘金寫的第一篇文章,花了幾天從新修改了一下。
排序就是將一組對象按照某種邏輯順序從新排列的過程。好比信用卡帳單中的交易是按照日期排序的——這種排序極可能使用了某種排序算法。在計算時代早期,你們廣泛認爲30%的計算週期都用在了排序上,今天這個比例可能下降了,大概是由於如今的排序算法更加高效。如今這個時代數據能夠說是無處不在,而整理數據的第一步每每就是進行排序。全部的計算機系統都實現了各類排序算法以供系統和用戶使用。
即便你只是使用標準庫中的排序函數,學習排序算法仍然有很大的實際意義:
另外,更重的是下面介紹的這些算法都很經典,優雅並且高效,學習其中的精髓對本身提升本身的編程能力也有很大的幫助。 排序在商業數據處理和現代科學計算中有很重要的地位,它可以應用於事務處理,組合優化,天體物理學,分子動力學,語言學,基因組學,天氣預報和不少其餘領域。下面會介紹的一種排序算法(快速排序)甚至被譽爲20世紀科學和工程領域的十大算法之一。後面咱們會依次學習幾種經典的排序算法,並高效地實現「優先隊列」這種基礎數據類型。咱們將討論比較排序算法的理論基礎並中借若干排序算法和優先隊列的應用。
圖片來源:維基百科
冒泡排序(Bubble Sort),是一種計算機科學領域的較簡單的排序算法。 它重複地走訪過要排序的數列,一次比較兩個元素,若是他們的順序錯誤就把他們交換過來。走訪數列的工做是重複地進行直到沒有再須要交換,也就是說該數列已經排序完成。這個算法的名字由來是由於越大的元素會經由交換慢慢「浮」到數列的頂端,故名「冒泡排序」。
比較相鄰的元素。若是第一個比第二個大,就交換他們兩個。 對每一對相鄰元素做一樣的工做,從開始第一對到結尾的最後一對。在這一點,最後的元素應該會是最大的數。針對全部的元素重複以上的步驟,除了最後一個。持續每次對愈來愈少的元素重複上面的步驟,直到沒有任何一對數字須要比較。
public static void bubbleSort(int[] numbers) {
int temp = 0;
int size = numbers.length;
for (int i = 0; i < size - 1; i++) {
for (int j = 0; j < size - 1 - i; j++) {
// 交換兩數位置
if (numbers[j] > numbers[j + 1])
{
temp = numbers[j];
numbers[j] = numbers[j + 1];
numbers[j + 1] = temp;
}
}
}
}
複製代碼
冒泡排序是最容易理解的一個排序算法。咱們實現完後,發現其效率並非很高。當序列已經有序的時候。會進行一些多餘的比較。根據其兩兩比較的特性,能夠推測出,若是一趟比較中連一次元素的交換操做都沒發生,那麼整個序列確定是已經有序的了。據此給出優化版冒泡排序算法。
public void bubbleSort(int[] numbers) {
// 初始化爲無序狀態
boolean sorted = false;
int temp = 0;
int size = numbers.length;
for (int i = 0; i < size - 1&& !sorted; i++) {
// 假設已經有序,若沒有發生交換,則sorted維持爲true,下次循環將直接退出。
sorted = true;
for (int j = 0; j < size - 1 - i; j++) {
if (numbers[j] > numbers[j + 1]) // 交換兩數位置
{
temp = numbers[j];
numbers[j] = numbers[j + 1];
numbers[j + 1] = temp;
// 數組無序
sorted = false;
}
}
}
}
複製代碼
簡單的對比圖:
排序用到的數據:
int[] numbers = { 1314, 920, 360 , 20863,3456,246437,234 ,11,23,3232,2323,4343,2131,221,312,321,3,123,21,321,321,3,123,21,321,3,213,21,321,312,3,21,1314, 920, 360 , 20863,3456,246437,234 ,11,23,3232,2323,4343,2131,221,312,321,3,123,21,321,321,3,123,21,321,3,213,21,321,312,3,21,1314, 920, 360 , 20863,3456,246437,234 ,11,23,3232,2323,4343,2131,221,312,321,3,123,21,321,321,3,123,21,321,3,213,21,321,312,3,21,1314, 920, 360 , 20863,3456,246437,234 ,11,23,3232,2323,4343,2131,221,312,321,3,123,21,321,321,3,123,21,321,3,213,21,321,312,3,21};
複製代碼
優化以前:
備註:程序運行時間和你的機器也有很大關係,大多數狀況下優化後的冒泡排序速度都更快一些。
圖片來源:維基百科
選擇排序是另外一個很容易理解和實現的簡單排序算法。學習它以前首先要知道它的兩個很鮮明的特色。1,運行時間和輸入無關。爲了找出最小的元素而掃描一遍數組並不能爲下一遍掃描提供任何實質性幫助的信息。所以使用這種排序的咱們會驚訝的發現,一個已經有序的數組或者數組內元素所有相等的數組和一個元素隨機排列的數組所用的排序時間居然同樣長!而其餘算法會更善於利用輸入的初始狀態,選擇排序則否則。 2,數據移動是最少的。選擇排序的交換次數和數組大小關係是線性關係。看下面的原理時能夠很容易明白這一點。
在要排序的一組數中,選出最小的一個數與第一個位置的數交換;而後在 剩下的數 當中再找最小的與第二個位置的數交換,如此循環到倒數第二個數和最後一個數比較爲止。
public static void selectSort(int[] numbers) {
int size = numbers.length; // 數組長度
int temp = 0; // 中間變量
for (int i = 0; i < size - 1; i++) {
// 選擇出應該在第i個位置的數也就是選出剩下元素最小的那個
for (int j = i; j < size; j++) {
if (numbers[j] < numbers[i]) {
// 交換兩個數
temp = numbers[i];
numbers[i] = numbers[j];
numbers[j] = temp;
}
}
}
}
複製代碼
圖片來源:維基百科
一般人們整理橋牌的方法是一張一張的來,將每一張牌插入到其餘已經有序牌中的適當位置。在計算機的實現中,爲了給要插入的元素騰出空間,咱們須要將其他全部元素在插入以前都向右移動一位。這種算法就叫,插入排序。 與選擇排序同樣,當前索引左邊的全部元素都是有序的,但他們的最終位置還不肯定爲了給更小的元素騰出空間,他們可能會被移動。可是當索引到達數組的右端時,數組排序就完成了。和選擇排序不一樣的是,插入排序所需的時間取決於輸入中元素的初始順序。也就是說對一個接近有序或有序的數組進行排序會比隨機順序或是逆序的數組進行排序要快的多。
每步將一個待排序的記錄,按其順序碼大小插入到前面已經排序的字序列的合適位置(從後向前找到合適位置後),直到所有插入排序完爲止。
以數組{38,65,97,76,13,27,49}爲例:
public static void insertSort(int[] numbers) {
int insertNote;// 要插入的數據
for (int i = 1; i < numbers.length; i++) {// 從數組的第二個元素開始循環將數組中的元素插入
insertNote = numbers[i];// 設置數組中的第2個元素爲第一次循環要插入的數據
int j = i - 1;
while (j >= 0 && insertNote < numbers[j]) {
numbers[j + 1] = numbers[j];// 若是要插入的元素小於第j個元素,就將第j個元素向後移動
j--;
}
numbers[j + 1] = insertNote;// 直到要插入的元素不小於第j個元素,將insertNote插入到數組中
}
}
複製代碼
圖片來源:維基百科
這個排序咋一看名字感受很高大上,這是以D.L.shell名字命名的排序算法。 爲了展現初級排序算法性質的價值,咱們來看一下基於插入排序的快速的排序算法——希爾排序。對於大規模亂序的數組插入排序很慢,由於它只會交換相鄰的元素,所以元素只能一點一點地從數組的一端移動到另外一端。若是最小的元素恰好在數組的盡頭的話,那麼要將它移動到正確的位置要N-1次移動。希爾排序爲了加快速度,簡單地改進了插入排序,交換不相鄰的元素以對數組的局部進行排序,並最終用插入排序將局部有序的數組排序。
先將整個待排序的記錄序列分割成爲若干子序列分別進行直接插入排序,待整個序列中的記錄「基本有序」時,再對全體記錄進行依次直接插入排序。
/** * 希爾排序是把記錄按下標的必定增量分組,對每組使用直接插入排序算法排序; * 隨着增量逐漸減小,每組包含的關鍵詞愈來愈多,當增量減至1時,整個文件恰被分紅一組,算法便終止。 * 經過某個增量將數組元素劃分爲若干組,而後分組進行插入排序,隨後逐步縮小增量,繼續按組進行插入排序操做,直至增量爲1 * * @param data */
public static void shellSort(int[] data) {
int j = 0;
int temp = 0;
// 每次將步長縮短爲原來的一半
for (int increment = data.length / 2; increment > 0; increment /= 2) {
// System.out.println("步長:" + increment);
for (int i = increment; i < data.length; i++) {
temp = data[i];
for (j = i; j >= increment; j -= increment) {
if (temp < data[j - increment])// 如想從小到大排只需修改這裏
{
data[j] = data[j - increment];
} else {
break;
}
}
data[j] = temp;
}
}
}
複製代碼
圖片來源:維基百科
歸併即將兩個有序的數組歸併併成一個更大的有序數組。人們很快根據這個思路發明了一種簡單的遞歸排序算法:歸併排序。要將一個數組排序,能夠先(遞歸地)將它分紅兩半分別排序,而後將結果歸併起來。歸併排序最吸引人的性質是它能保證任意長度爲N的數組排序所需時間和NlogN成正比;它的主要缺點也顯而易見就是它所需的額外空間和N成正比。簡單的歸併排序以下圖:
![]()
歸併(Merge)排序法是將兩個(或兩個以上)有序表合併成一個新的有序表,即把待排序序列分爲若干個子序列,每一個子序列是有序的。而後再把有序子序列合併爲總體有序序列。
合併方法:
設r[i…n]由兩個有序子表r[i…m]和r[m+1…n]組成,兩個子表長度分別爲n-i +一、n-m。
一、j=m+1;k=i;i=i; //置兩個子表的起始下標及輔助數組的起始下標
二、若i>m 或j>n,轉⑷ //其中一個子表已合併完,比較選取結束
三、//選取r[i]和r[j]較小的存入輔助數組rf
若是r[i]<r[j],rf[k]=r[i]; i++; k++; 轉⑵
不然,rf[k]=r[j]; j++; k++; 轉⑵
四、//將還沒有處理完的子表中元素存入rf
若是i<=m,將r[i…m]存入rf[k…n] //前一子表非空
若是j<=n , 將r[j…n] 存入rf[k…n] //後一子表非空
五、合併結束。
複製代碼
/** * 歸併排序 簡介:將兩個(或兩個以上)有序表合併成一個新的有序表 * 即把待排序序列分爲若干個子序列,每一個子序列是有序的。而後再把有序子序列合併爲總體有序序列 時間複雜度爲O(nlogn) 穩定排序方式 * * @param nums * 待排序數組 * @return 輸出有序數組 */
public static int[] mergeSort(int[] nums, int low, int high) {
int mid = (low + high) / 2;
if (low < high) {
// 左邊
mergeSort(nums, low, mid);
// 右邊
mergeSort(nums, mid + 1, high);
// 左右歸併
merge(nums, low, mid, high);
}
return nums;
}
/** * 將數組中low到high位置的數進行排序 * * @param nums * 待排序數組 * @param low * 待排的開始位置 * @param mid * 待排中間位置 * @param high * 待排結束位置 */
public static void merge(int[] nums, int low, int mid, int high) {
int[] temp = new int[high - low + 1];
int i = low;// 左指針
int j = mid + 1;// 右指針
int k = 0;
// 把較小的數先移到新數組中
while (i <= mid && j <= high) {
if (nums[i] < nums[j]) {
temp[k++] = nums[i++];
} else {
temp[k++] = nums[j++];
}
}
// 把左邊剩餘的數移入數組
while (i <= mid) {
temp[k++] = nums[i++];
}
// 把右邊邊剩餘的數移入數組
while (j <= high) {
temp[k++] = nums[j++];
}
// 把新數組中的數覆蓋nums數組
for (int k2 = 0; k2 < temp.length; k2++) {
nums[k2 + low] = temp[k2];
}
}
複製代碼
圖片來源:維基百科
快速排序多是應用最普遍的排序算法了。快速排序流行的緣由主要由於它實現簡單,適用於不一樣的輸入數據且在通常應用中比其餘排序算法都要快得多。快速排序引人注目的特色包括它是原地排序(只須要一個很小的輔助棧),且將長度爲N的數組排序所須要的時間和NlgN成正比。咱們以前提到的幾種排序算法都沒法將這兩個優勢結合起來。另外,快速排序的內循環比大多數排序算法都要短小,這意味着它不管是理論上仍是實際中都要更快。它的主要缺點是很是脆弱,在實現時要很是當心才能避免低劣的性能。已經有無數例子顯示許多錯誤都能導致它在實際應用中的性能只有平方級別。不過還好,咱們由這些缺點和教訓中大大改進了快速排序算法,使它的應用更加普遍。
快速排序是一種分治的排序算法。它將一個數組分紅兩個字數組,將兩部分獨立地排序。 快速排序和歸併排序是互補的:歸併排序將數組分紅兩個字數組分別排序,並將有序的字數組歸併以將整個數組排序;而快速排序將數組排序的方式則是當兩個字數組都有序時整個數組也就天然有序了。在第一種狀況中,遞歸調用發生在處理整個數組以前;在第二種狀況中,遞歸調用發生在處理整個數組以後。在歸併排序中,一個數組被等分爲兩半;快速排序中,切分的位置取決於數組的內容。快速排序的過程大體以下:
![]()
快速排序的基本思想:
經過一趟排序將待排序記錄分割成獨立的兩部分,一部分全小於選取的參考值,另外一部分全大於選取的參考值。這樣分別對兩部分排序以後順序就能夠排好了。 例子:
(a)一趟排序的過程:
/** * 查找出中軸(默認是最低位low)的在numbers數組排序後所在位置 * * @param numbers 帶查找數組 * @param low 開始位置 * @param high 結束位置 * @return 中軸所在位置 */
public static int getMiddle(int[] numbers, int low, int high) {
// 數組的第一個做爲中軸
int temp = numbers[low];
while (low < high) {
while (low < high && numbers[high] > temp) {
high--;
}
// 比中軸小的記錄移到低端
numbers[low] = numbers[high];
while (low < high && numbers[low] < temp) {
low++;
}
// 比中軸大的記錄移到高端
numbers[high] = numbers[low];
}
numbers[low] = temp; // 中軸記錄到尾
return low; // 返回中軸的位置
}
/** * * @param numbers 帶排序數組 * @param low 開始位置 * @param high 結束位置 */
public static void quick(int[] numbers, int low, int high) {
if (low < high) {
int middle = getMiddle(numbers, low, high); // 將numbers數組進行一分爲二
quick(numbers, low, middle - 1); // 對低字段表進行遞歸排序
quick(numbers, middle + 1, high); // 對高字段表進行遞歸排序
}
}
/** * 快速排序 * 快速排序提供方法調用 * @param numbers 帶排序數組 */
public static void quickSort(int[] numbers) {
// 查看數組是否爲空
if (numbers.length > 0)
{
quick(numbers, 0, numbers.length - 1);
}
}
複製代碼
咱們只介紹一種經常使用的,具體代碼就不貼出了。想去的能夠去https://algs4.cs.princeton.edu/code/ 這個網站找。
三向切分快速排序 :
核心思想就是將待排序的數據分爲三部分,左邊都小於比較值,右邊都大於比較值,中間的數和比較值相等.三向切分快速排序的特性就是遇到和比較值相同時,不進行數據交換, 這樣對於有大量重複數據的排序時,三向切分快速排序算法就會優於普通快速排序算法,但因爲它總體判斷代碼比普通快速排序多一點,因此對於常見的大量非重複數據,它並不能比普通快速排序多大多的優點 。
堆排序(Heapsort)是指利用堆積樹(堆)這種數據結構所設計的一種排序算法,它是是對簡單選擇排序的改進。。能夠利用數組的特色快速定位指定索引的元素。堆分爲大根堆和小根堆,是徹底二叉樹。大根堆的要求是每一個節點的值都不大於其父節點的值,即A[PARENT[i]] >= A[i]。在數組的非降序排序中,須要使用的就是大根堆,由於根據大根堆的要求可知,最大的值必定在堆頂。
將待排序的序列構形成一個大頂堆。此時,整個序列的最大值就是堆頂的根節點。將它移走(其實就是將其與堆數組的末尾元素交換,此時末尾元素就是最大值),而後將剩餘的n-1個序列從新構形成一個堆,這樣就會獲得n個元素中的次最大值。如此反覆執行,就能獲得一個有序序列了。
堆節點的訪問
一般堆是經過一維數組來實現的。在數組起始位置爲0的情形中:
堆的操做
在堆的數據結構中,堆中的最大值老是位於根節點(在優先隊列中使用堆的話堆中的最小值位於根節點)。堆中定義如下幾種操做:
public class HeapSort {
private int[] arr;
public HeapSort(int[] arr){
this.arr = arr;
}
/** * 堆排序的主要入口方法,共兩步。 */
public void sort(){
/* * 第一步:將數組堆化 * beginIndex = 第一個非葉子節點。 * 從第一個非葉子節點開始便可。無需從最後一個葉子節點開始。 * 葉子節點能夠看做已符合堆要求的節點,根節點就是它本身且本身如下值爲最大。 */
int len = arr.length - 1;
int beginIndex = (len - 1) >> 1;
for(int i = beginIndex; i >= 0; i--){
maxHeapify(i, len);
}
/* * 第二步:對堆化數據排序 * 每次都是移出最頂層的根節點A[0],與最尾部節點位置調換,同時遍歷長度 - 1。 * 而後重新整理被換到根節點的末尾元素,使其符合堆的特性。 * 直至未排序的堆長度爲 0。 */
for(int i = len; i > 0; i--){
swap(0, i);
maxHeapify(0, i - 1);
}
}
private void swap(int i,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
/** * 調整索引爲 index 處的數據,使其符合堆的特性。 * * @param index 須要堆化處理的數據的索引 * @param len 未排序的堆(數組)的長度 */
private void maxHeapify(int index,int len){
int li = (index << 1) + 1; // 左子節點索引
int ri = li + 1; // 右子節點索引
int cMax = li; // 子節點值最大索引,默認左子節點。
if(li > len) return; // 左子節點索引超出計算範圍,直接返回。
if(ri <= len && arr[ri] > arr[li]) // 先判斷左右子節點,哪一個較大。
cMax = ri;
if(arr[cMax] > arr[index]){
swap(cMax, index); // 若是父節點被子節點調換,
maxHeapify(cMax, len); // 則須要繼續判斷換下後的父節點是否符合堆的特性。
}
}
/** * 測試用例 * * 輸出: * [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9] */
public static void main(String[] args) {
int[] arr = new int[]{3,5,3,0,8,6,1,5,8,6,2,4,9,4,7,0,1,8,9,7,3,1,2,5,9,7,4,0,2,6};
new HeapSort(arr).sort();
System.out.println(Arrays.toString(arr));
}
}
複製代碼
若是一個排序算法可以保留數組中重複元素的相對位置則能夠被稱爲是穩定的。這個性質在許多狀況下很重要。例如:在考慮一個須要處理大量含有地理位置和時間戳的事件互聯網商業應用程序。首先,咱們在時間發生時將它們挨個存儲在一個數組中,這樣在數組中它們已是按照時間順序排序好了的。如今假設在進一步處理前將按照地理位置劃分。一種簡單的方法是將數組按照位置排序。若是排序算法不是穩定的,排序後的每一個城市的交易可能不會再是按照時間順序排序的了。不少狀況下,不熟悉排序穩定性的程序員在第一次遇到這種情形的時候會在使用不穩定排序算法後把數組弄的一團糟而一臉懵逼。上一篇文章中咱們講到的插入排序和歸併排序都屬於穩定的,其餘幾個(選擇,希爾,快速,堆排序)都是不穩定的。有不少辦法可以將任意排序算法編程穩定的,但通常只有在穩定性是必要的狀況下穩定的排序算法纔有優點。不要想固然認爲算法具備穩定性是理所固然的,但事實上沒有任何實際應用中常見的算法不是用了大量額外的時間和空間才作到了這一點(研究人員開發了這樣的算法,但應用程序員發現它們太複雜了,沒法在實際開發中使用)。 上述例子以下圖所示:
下表總結了咱們面試常見的排序算法的各類重要性質。除了希爾排序(它的複雜度只是一個近似),插入排序(它的複雜度取決於輸入元素的排列狀況)和快速排序的兩個版本(它門的複雜度和機率有關,取決於輸入元素的分不清狀況)以外,將這些運行時間的增加數量級乘以適當的常數就可以大體估計出其運行時間。這裏的常數有時和算法有關(好比堆排序的比較次數是歸併排序的兩倍,且二者訪問數組的次數都比快速排序多得多),但主要取決於算法的實現,Java編譯器以及你的計算機,這些因素決定了須要執行的機器指令的數量以及每條指令所需的執行時間。最重要的是,由於這些都是常數,你能經過較小的N獲得的實現數據和咱們標準雙倍測試來推測較大N所需的運行時間。
Java系統庫主要排序方法java.util.Arrays.sort()。更具不一樣的參數類型,它實際表明了一系列排序方法:
package map;
import java.util.Set;
import java.util.TreeMap;
public class TreeMap2 {
public static void main(String[] args) {
// TODO Auto-generated method stub
TreeMap<Person, String> pdata = new TreeMap<Person, String>();
pdata.put(new Person("張三", 30), "zhangsan");
pdata.put(new Person("李四", 20), "lisi");
pdata.put(new Person("王五", 10), "wangwu");
pdata.put(new Person("小紅", 5), "xiaohong");
//獲得key的值的同時獲得key所對應的值
Set<Person> keys = pdata.keySet();
for (Person key : keys) {
System.out.println(key.getAge() + "-" + key.getName());
}
}
}
// person對象沒有實現Comparable接口,因此必須實現,這樣纔不會出錯,纔可使treemap中的數據按順序排列
// 前面一個例子的String類已經默認實現了Comparable接口,詳細能夠查看String類的API文檔,另外其餘
// 像Integer類等都已經實現了Comparable接口,因此不須要另外實現了
class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
/** *TODO重寫compareTo方法實現按年齡來排序 */
@Override
public int compareTo(Person o) {
// TODO Auto-generated method stub
if (this.age > o.getAge()) {
return 1;
} else if (this.age < o.getAge()) {
return -1;
}
return age;
}
}
複製代碼
參考:
《算法》
維基百科:https://zh.wikipedia.org/wiki/堆排序
歡迎關注個人微信公衆號(堅持原創,分享美文,分享各類Java學習資源,面試題,以及企業級Java實戰項目回覆關鍵字免費領取):