之前學習數據結構與算法時花了三天時間整理九大排序算法,並採用Java語言來實現,今天第一次寫博客,剛好可以把這些東西從總結的文檔中拿出來與大家分享一下,同時作爲自己以後的備忘錄。
1.排序算法時間複雜度、穩定性分類:
2.排序算法問題描述與實現
2.1冒泡排序(交換排序-穩定)
【問題描述】對於一個int數組,請編寫一個冒泡排序算法,對數組元素排序。
問題分析:冒泡排序,顧名思義,從前往後遍歷,每次遍歷在末尾固定一個最大值。
易錯點:每次內層循環結束都會在末尾確定一個元素的位置,因此內層循環的判斷條件要減去外層的循環次數i
public int[] BobbleSort(int[] array){
for(int i=0;i<array.length-1;i++){
for(int j=0;j<array.length-1-i;j++){ //內層循環,注意array.length-1-i,一定要-i
swap(array,j,j+1);//交換數據元素的方法
}
}
return array;
}
public void swap(int[] array,int i,int j){//交換數據元素的方法
if(array[i]>array[j]){
int temp=array[i];
array[i]=array[j];
array[j]=temp;
}
}
冒泡排序的改進:加一個布爾型的flag,當某一趟冒泡排序沒有元素交換時,則冒泡結束,元素已經有序,可以有效的減少冒泡次數。
2.2 選擇排序(不穩定)
【問題描述】對於一個int數組,請編寫一個選擇排序算法,對數組元素排序。
問題分析:選擇排序,從前往後遍歷,每次遍歷從頭開始依次選擇最小的來排序。
注意點:
【初始升序】:交換0次,時間複雜度爲o(n); 【初始降序】:交換n-1次,時間複雜度爲o(n^2)。
【特點】:交換移動數據次數少,比較次數多。
public int[] choose(int[] array){
for(int i=0;i<array.length-1;i++){
int min=i;
for (int j = i+1; j < array.length; j++) {
if(array[min]>array[j])
min=j;
}
if(i!=min){
swap(array,i,min);
}
}
return array;
}
public void swap(int[] array,int i,int j){
if(array[i]>array[j]){
int temp=array[i];
array[i]=array[j];
array[j]=temp;
}
}
2.3 直接插入排序(穩定的 )
【問題描述】對於一個int數組,請編寫一個插入排序算法,對數組元素排序。
問題分析:插入排序,從前往後遍歷,每次遍歷確定一個元素的位置,下一個元素從後往前依次與排序好的元素比較,類似於摸撲克牌。
注意點:假定n是數組的長度,首先假設第一個元素被放置在正確的位置上,這樣僅需從1-n-1範圍內對剩餘元素進行排序。對於每次遍歷,從0-i-1範圍內21.的元素已經被排好序,每次遍歷的任務是:通過掃描前面已排序的子列表,將位置i處的元素定位到從0到i的子列表之內的正確的位置上。將arr[i]複製爲一個名爲target的臨時元素。向下掃描列表,比較這個目標值target與arr[i-1]、arr[i-2]的大小,依次類推。這個比較過程在小於或等於目標值的第一個元素(arr[j])處停止,或者在列表開始處停止(j=0)。在arr[i]小於前面任何已排序元素時,後一個條件(j=0)爲真,因此,這個元素會佔用新排序子列表的第一個位置。在掃描期間,大於目標值target的每個元素都會向右滑動一個位置(arr[j]=arr[j-1])。一旦確定了正確位置j,目標值target(即原始的arr[i])就會被複制到這個位置。與選擇排序不同的是,插入排序將數據向右滑動,並且不會執行交換。
public int[] insert1(int[] array){
for(int i=1;i<array.length;i++){
int j=i;
int temp=array[i];
while(j>0&&array[j-1]>temp){
array[j]=array[j-1];//元素右移
j--;
}
array[j]=temp;
}
return array;
}
2.4 希爾排序(2個for循環+最後一個while,不穩定)
問題描述:對於一個int數組,請編寫一個希爾排序算法,對數組元素排序。
問題分析:希爾排序是改進的插入排序,算法先將要排序的一組數按某個增量d(n/2,n爲要排序數的個數)分成若干組,每組中記錄的下標相差d.對每組中全部元素進行直接插入排序,然後再用一個較小的增量(d/2)對它進行分組,在每組中再進行直接插入排序。當增量減到1時,進行直接插入排序後,排序完成。希爾排序法(縮小增量法) 屬於插入類排序,是將整個無序列分割成若干小的子序列分別進行插入排序的方法。
- public int[] hillInsert(int[] array){
- for (int d = array.length>>1; d>=1; d=d>>1) {
- for(int i=d;i<array.length;i++){
- int j=i;
- int temp=array[i];
- while(j>=d&&array[j-d]>temp){
- array[j]=array[j-d];
- j-=d;
- }
- array[j]=temp;
- }
- }
- return array;
- }
2.5.堆排序(不穩定)
問題描述:對於一個int數組,請編寫一個堆排序算法,對數組元素排序。
問題分析:堆數據結構是一種數組對象,它可以被視爲一科完全二叉樹結構(完全二叉樹是由滿二叉樹而引出來的。對於深度爲K的,有n個結點的二叉樹,當且僅當其每一個結點都與深度爲K的滿二叉樹中編號從1至n的結點一一對應時稱之爲完全二叉樹)。它的特點是父節點的值大於(小於)兩個子節點的值(分別稱爲大頂堆和小頂堆)。它常用於管理算法執行過程中的信息,應用場景包括堆排序,優先隊列等。
完全二叉樹性質:如果i>1,則雙親是結點[i/2]。也就是說下標i與2i和2i+1是雙親子女關係。 當排序對象爲數組時,下標從0開始,所以下標 i 與下標 2i+1和2i+2是雙親子女關係。
算法思路:
- 堆排序:(大根堆)
- ①將存放在array[0,...,n-1]中的n個元素建成初始堆;
- ②將堆頂元素與堆底元素進行交換,則序列的最大值即已放到正確的位置;
- ③但此時堆被破壞,將堆頂元素向下調整使其繼續保持大根堆的性質,再重複第②③步,直到堆中僅剩下一個元素爲止。
- 堆排序算法的性能分析:
- 空間複雜度:o(1);
- 時間複雜度:建堆:o(n),每次調整o(log n),故最好、最壞、平均情況下:o(n*logn);
- 穩定性:不穩定
- public static int[] heapMax(int[] array){ //建立大根堆
- for(int i=(array.length-2)/2;i>=0;i--){
- buildHeap(array,i,array.length);
- }
- return array;
- }
- private static void buildHeap(int[] array, int k, int length) { //建堆方法
- // TODO Auto-generated method stub
- int temp=array[k];
- for(int i=2*k+1;i<length-1;i=2*i+1){
- if(array[i]<array[i+1]){
- i++;
- }
- if(temp>=array[i])
- break;
- else{
- array[k]=array[i];
- k=i;
- }
- }
- array[k]=temp;
- }
- public static int[] heapSortArray(int[] array){ //進行堆排序
- array=heapMax(array);
- for (int i =array.length-1; i >0; i--) {
- int temp=array[0];
- array[0]=array[i];
- array[i]=temp;
- buildHeap(array,0,i);
- }
- return array;
- }
2.6 歸併排序(穩定)
問題描述:對於一個int數組,請編寫一個歸併排序算法,對數組元素排序。
問題分析:歸併排序是建立在歸併操作上的一種有效的排序算法。該算法是採用分治法(Divide and Conquer)的一個非常典型的應用。首先考慮下如何將將二個有序數列合併。這個非常簡單,只要從比較二個數列的第一個數,誰小就先取誰,取了後就在對應數列中刪除這個數。然後再進行比較,如果有數列爲空,那直接將另一個數列的數據依次取出即可。可以看出合併有序數列的效率是比較高的,可以達到O(n)。
解決了上面的合併有序數列問題,再來看歸併排序,其的基本思路就是將數組分成二組A,B,如果這二組組內的數據都是有序的,那麼就可以很方便的將這二組數據進行排序。如何讓這二組組內數據有序了?
可以將A,B組各自再分成二組。依次類推,當分出來的小組只有一個數據時,可以認爲這個小組組內已經達到了有序,然後再合併相鄰的二個小組就可以了。這樣通過先遞歸的分解數列,再合併數列就完成了歸併排序。
public int[] mergeSort(int[] A, int n) {
//歸併排序,遞歸做法,分而治之
mSort(A,0,n-1);
return A;
}
private void mSort(int[] A,int left,int right){
//分而治之,遞歸常用的思想,跳出遞歸的條件
if(left>=right){
return;
}
//中點
int mid = (left+right)/2;
//有點類似後序遍歷!
mSort(A,left,mid);
mSort(A,mid+1,right);
merge(A,left,mid,right);
}
//將左右倆組的按序子序列排列成按序序列
private void merge(int[] A,int left,int mid,int rightEnd){
//充當tem數組的下標
int record = left;
//最後複製數組時使用
int record2 = left;
//右子序列的開始下標
int m =mid+1;
int[] tem = new int[A.length];
//只要left>mid或是m>rightEnd,就跳出循環
while(left<=mid&&m<=rightEnd){
if(A[left]<=A[m]){
tem[record++]=A[left++];
}else{
tem[record++]=A[m++];
}
}
while(left<=mid){
tem[record++]=A[left++];
}
while(m<=rightEnd){
tem[record++]=A[m++];
}
//複製數組
for( ;record2<=rightEnd;record2++){
A[record2] = tem[record2];
}
}
2.7 快速排序算法(不穩定)
問題描述:對於一個int數組,請編寫一個快速排序算法,對數組元素排序。
問題分析:快速排序(Quicksort)是對冒泡排序的一種改進,使用分治法(Divide and conquer)策略來把一個序列(list)分爲兩個子序列(sub-lists)。
從數列中挑出一個元素,稱爲」樞軸」(pivot)。重新排序數列,所有元素比樞軸值小的擺放在基準前面,所有元素比樞軸值大的擺在樞軸的後面(相同的數可以到任一邊)。
在這個分區結束之後,該樞軸就處於數列的中間位置。這個稱爲分區(partition)操作。遞歸地(recursive)把小於樞軸值元素的子數列和大於樞軸值元素的子數列排序。
- public static final void quickSort(int[] array, intstart, intend) {
- // i相當於助手1的位置,j相當於助手2的位置
- int i = start, j = end;
- int pivot = array[i]; // 取第1個元素爲基準元素
- int emptyIndex = i; // 表示空位的位置索引,默認爲被取出的基準元素的位置
- // 如果需要排序的元素個數不止1個,就進入快速排序(只要i和j不同,就表明至少有2個數組元素需要排序)
- while (i < j) {
- // 助手2開始從右向左一個個地查找小於基準元素的元素
- while (i < j && pivot <= array[j])
- j--;
- if (i < j) {
- // 如果助手2在遇到助手1之前就找到了對應的元素,就將該元素給助手1的"空位",j成了空位
- array[emptyIndex] = array[emptyIndex = j];
- }
- // 助手1開始從左向右一個個地查找大於基準元素的元素
- while (i < j && array[i] <= pivot)
- i++;
- if (i < j) {
- // 如果助手1在遇到助手2之前就找到了對應的元素,就將該元素給助手2的"空位",i成了空位
- array[emptyIndex] = array[emptyIndex = i];
- }
- }
- // 助手1和助手2相遇後會停止循環,將最初取出的基準值給最後的空位
- array[emptyIndex] = pivot;
- // =====本輪快速排序完成=====
- // 如果分割點i左側有2個以上的元素,遞歸調用繼續對其進行快速排序
- if (i - start > 1) {
- quickSort(array, 0, i - 1);
- }
- // 如果分割點j右側有2個以上的元素,遞歸調用繼續對其進行快速排序
- if (end - j > 1) {
- quickSort(array, j + 1, end);
- }
- }
算法優化:選取基準軸點時採用三數取中法:
public class Quick {
public void sort(int[] array){
int start=0;
int end=array.length-1;
quickSort(array,start,end);
}
public void quickSort(int[] array, int start, int end) {
// TODO Auto-generated method stub
int i=start;
int j=end;
int emptyIndex=start;
int pivot=middle3(array,start,end);
while(i<j){
while(i<j&&array[j]>=pivot){
j--;
}
if(i<j)
array[emptyIndex]=array[emptyIndex=j];
while(i<j&&array[i]<=pivot){
i++;
}
if(i<j)
array[emptyIndex]=array[emptyIndex=i];
}
array[emptyIndex]=pivot;
if(i-start>1)
quickSort(array,start,i-1);
if(end-j>1)
quickSort(array,j+1,end);
}
2.8 計數排序算法(穩定,也是桶排序)
問題描述:對於一個int數組,請編寫一個計數排序算法,對數組元素排序。
問題分析:
1. 提前必須是已知待排序的關鍵字爲整型且範圍已知。
2. 時間複雜度爲O(n+k),n指的是桶的個數,k指的是待排序數組的長度,不是基於比較的排序算法,因此效率非常之高。
3. 穩定性好,這個是計數排序非常重要的特性,可以用在後面介紹的基數排序中。
4. 但需要一些輔助數組,如C[0..k],因此待排序的關鍵字範圍0~k不宜過大。
public int[] countingSort(int[] A, int n) {
if(A==null ||n<2){
return A;
}
//找出桶的範圍,即通過要排序的數組的最大最小值來確定桶範圍
int min=A[0];
int max=A[0];
for(int i=0;i<n;i++){
min=Math.min(A[i],min);
max=Math.max(A[i],max);
}
//確定桶數組,桶的下標即爲需排序數組的值,桶的值爲序排序數同一組值出現的次數
int[] arr = new int[max-min+1];
//往桶裏分配元素
for(int i=0;i<n;i++){
arr[A[i]-min]++;
}
//從桶中取出元素
int index=0;
for(int i=0;i<arr.length;i++){
while(arr[i]-->0){
A[index++]=i+min;
}
}
return A;
}
}
2.9 基數排序算法(穩定)
問題描述:對於一個int數組,請編寫一個基數排序算法,對數組元素排序。
問題分析:
基數排序(Radix sort)是一種非比較型整數排序算法,其原理是將整數按位數切割成不同的數字,然後按每個位數分別比較。由於整數也可以表達字符串(比如名字或日期)和特定格式的浮點數,所以基數排序也不是隻能使用於整數。將所有待比較數值(正整數)統一爲同樣的數位長度,數位較短的數前面補零。然後,從最低位開始,依次進行一次排序。這樣從最低位排序一直到最高位排序完成以後,數列就變成一個有序序列。
鍵字範圍0~k不宜過大。
public void sort(int[] array){
int max=0;
int d=0;// 位數
for (int i = 0; i < array.length; i++) {
max=Math.max(max, array[i]);
}
while(max>0){
max/=10;
d++;
}
int t=0;
int m=1;
int n=1;
int[][] temp=new int[10][array.length];
int[] order=new int[10];
while(m<=d){
for (int i = 0; i < array.length; i++) {
int k=((array[i]/n)%10);
temp[k][order[k]++]=array[i];
}
for (int i = 0; i < 10; i++) {
if(order[i]!=0){
for (int j = 0; j <order[i]; j++) {
array[t++]=temp[i][j];
}
order[i]=0;
}
}
t=0;
n=n*10;
m++;
}
}
採用鏈表的方式來實現基數排序:
//基數排序開始
public static void radixSort(int[] array){
int max=array[0];
for (int i = 0; i < array.length; i++) {
max=Math.max(max, array[i]);
}
int time=0;
while(max!=0){
time++;
max=max/10;
}
List<List<Integer>> list=new ArrayList<List<Integer>>();
for(int i=0;i<10;i++){
List<Integer> item=new ArrayList<Integer>();
list.add(item);
}
for (int i = 0; i < time; i++) {
for (int j = 0; j < array.length; j++) {
int index=array[j]%(int)Math.pow(10, i+1);
index/=(int)Math.pow(10, i);
list.get(index).add(array[j]);
}
int count=0;
for (List<Integer> a : list) {
for (int m : a) {
if(m!=0){
array[count++]=m;
}
}
a.clear();
}
}
}
//基數排序結束