排序演算法-八大排序實現和效能比較
概述
常見的八大排序演算法,他們之間關係如下:
本文的開發語言是用Java,為了更好的演示,這裡先新建一個工具類NumberUtils ,用來生成隨機陣列和列印排序前後陣列的內容。
public class NumberUtils {
/**
* 獲取隨機int型別陣列
*/
public static int[] getRandomArs(int length,int max){
int rs[]=new int[length];
Random random=new Random();
for (int i=0;i<length;i++){
rs[i]=random.nextInt(max);
}
return rs ;
}
/**
* 列印內容
*/
public static void display(int intArrays[],int type){
int count=0;
if(type==2){
System.out.print("排序後:");
}else if(type==1){
System.out.print("排序前:" );
}
if(intArrays.length<40){
for(int i:intArrays){
System.out.print(i+" ");
}
}else {
for(int i:intArrays){
count++;
if(count<10){
System.out.print(i+" ");
}else if(count==10){
System.out.print("......");
}else if(count> intArrays.length-10){
System.out.print(i+" ");
}
}
}
System.out.println();
}
/**
* 交換陣列中兩個數的位置
*/
public static void exchange(int[] a, int i, int j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
插入排序
直接插入排序
直接插入排序是一種簡單插入排序,基本思想是:把n個待排序的元素看成為一個有序表和一個無序表。開始時有序表中只包含1個元素,無序表中包含有n-1個元素,排序過程中每次從無序表中取出第一個元素,將它插入到有序表中的適當位置,使之成為新的有序表,重複n-1次可完成排序過程
類似我們摸牌,一開始有一堆牌(待排序的)。由於第一次摸牌時手中沒牌,所以不需要排序。第二次摸牌時和手中第一張拍比較,如果它大,就放在它的後面。 每次摸牌都會把牌放在一個前面比自己小(或等於),後面比自己大(或等於)的位置。
直接插入排序的Java程式碼
public class Insertion {
/**
* 直接插入排序演算法方法
*/
public static void sort(int intArrays[]){
int length=intArrays.length;
int i,j;
for(i=1;i<length;i++){
for(j=i-1;j>=0&&intArrays[i]<intArrays[j];j--){
//intArrays[i]代表要插入的數字,intArrays[j]代表需要比較大小的數字,j遞減
//當intArrays[i]大於intArrays[j]時(intArrays[i]插入的位置,也就是說插在j的後一位)或者intArrays[i]為當前陣列的最小值時(此時的j為-1,也就是說intArrays[i]要插在第一位)返回j
}
//將intArrays[i]儲存住,因為要j以後的陣列向後移一位
int temp=intArrays[i];
for(int k=i;k>j+1;k--){
//將i到j範圍的陣列向後移一位
intArrays[k]=intArrays[k-1];
}
//intArrays[i]插在j的後一位
intArrays[j+1]=temp;
}
}
/**
* 執行入口,intArrays:待排序的陣列,displaySort:是否顯示排序前和排序後的內容
*/
public static void run(int intArrays[],boolean displaySort){
int arrays[]= intArrays.clone();
if(displaySort){
NumberUtils.display(arrays,1);
}
long startTime=System.currentTimeMillis();
sort(arrays);
long endTime=System.currentTimeMillis();
if(displaySort){
NumberUtils.display(arrays,2);
}
System.out.println("插入排序用時:"+(endTime-startTime)+"毫秒");
}
/**
* 測試排序用的主方法
*/
public static void main(String[] args){
//陣列長度
int length=30000;
//最大值
int max =100000000;
//是否列印排序後的內容
boolean display=true;
//隨機獲取的排序陣列
int intArrays[]= NumberUtils.getRandomArs(length,max);
//插入排序
Insertion.run(intArrays,display);
}
}
執行main方法,終端輸出
排序前:52096862 89211005 17092175 60370534 40302430 53897879 34127111 43318378 29195095 ......44543512 40238517 99995921 49109471 44841738 2383202 81685348 82456235 16748589 90004692
排序後:1432 1466 8523 10963 16793 17166 17239 20386 23581 ......99961843 99967372 99967545 99967909 99979307 99979543 99981231 99984869 99995921 99998178
插入排序用時:308毫秒
希爾排序
該方法的基本思想是:先將整個待排元素序列分割成若干個子序列(由相隔某個“增量”的元素組成的)分別進行直接插入排序,然後依次縮減增量再進行排序,待整個序列中的元素基本有序(增量足夠小)時,再對全體元素進行一次直接插入排序。因為直接插入排序在元素基本有序的情況下(接近最好情況),效率是很高的,因此希爾排序在時間效率上比前兩種方法有較大提高。
希爾排序的Java程式碼
public class Shell {
public static void sort(int[] intArrays) {
int length = intArrays.length;
int h = 1;
int block=3;//分塊大小(大於1的值)
//h為分割槽後每塊有多少個元素
while (h < length / block) {
h = block* h + 1; //通過迴圈算出h的取值,當分割槽大小為3時,h序列為1, 4, 13, 40, ...
}
while (h >= 1) {
int n, i ,j, k;
//分割後,產生n個子序列
for (n = 0; n < h; n++) {
//分別對每個子序列進行直接插入排序
for (i = n + h; i < length; i += h) {
for (j = i - h; j >= 0 && intArrays[i] < intArrays[j]; j -= h) {
}
int tmp = intArrays[i];
for (k = i; k > j + h; k -= h) {
intArrays[k] = intArrays[k-h];
}
intArrays[j+h] = tmp;
}
}
//直接插入排序完後,減少每塊區裡的元素。也就是說增大塊區的數量,直到最後h=1(每塊區裡只有一個元素時,排序完成)
h = h / block;
}
}
/**
* 執行入口,intArrays:待排序的陣列,displaySort:是否顯示排序前和排序後的內容。
*/
public static void run(int intArrays[],boolean displaySort){
//克隆一份陣列
int arrays[]= intArrays.clone();
// 判斷是否需要顯示排序前的內容
if(displaySort){
NumberUtils.display(arrays,1);
}
// 記錄開始時間
long startTime=System.currentTimeMillis();
sort(arrays);
// 記錄結束時間
long endTime=System.currentTimeMillis();
// 判斷是否需要顯示排序前的內容
if(displaySort){
NumberUtils.display(arrays,2);
}
System.out.println("希爾排序用時:"+(endTime-startTime)+"毫秒");
}
/**
* 測試排序用的主方法
*/
public static void main(String[] args){
//陣列長度
int length=30000;
//最大值
int max =100000000;
//是否列印排序後的內容
boolean display=true;
//隨機獲取的排序陣列
int intArrays[]= NumberUtils.getRandomArs(length,max);
//希爾排序
Shell.run(intArrays,display);
}
}
執行main方法,終端輸出
排序前:5899233 16208367 81634733 42495727 1153823 76149452 14140770 48392747 88266082 ......25538919 94969077 11926461 62607362 11544024 8288043 43560779 20729207 53683969 31870096
排序後:6579 12358 12735 14630 18684 33376 38992 46903 51886 ......99980604 99982870 99985950 99987490 99987599 99994174 99995824 99996256 99997231 99999736
希爾排序用時:20毫秒
選擇排序
簡單選擇排序
從待排序序列中,找到關鍵字最小的元素,如果最小元素不是待排序序列的第一個元素,將其和第一個元素互換。從餘下的 N - 1 個元素中,找出關鍵字最小的元素,重複(1)、(2)步,直到排序結束。
選擇排序的Java程式碼:
public class Selection {
public static void sort(int intArrays[]){
int length=intArrays.length;//陣列長度
//迴圈比較,假如陣列長度為10,則迴圈9次。第一趟迴圈有10個數,找出這十個數的最小值放在一位。第二次迴圈找出另外9個數的最小值,放在數字第二位。第三次找出另外8個數最小值,以此類推...
for(int i=0;i<length-1;i++){
//用來儲存每一趟最小值陣列的下標,開始前假設第一個數字為這趟的最小值
int minIndex=i;
//找出每一趟的最小值,如果有最小值就將這趟的最小值放在這趟陣列第一個位,如果沒有最小值就繼續執行下一趟
for(int j=i+1;j<length;j++){
if(intArrays[j]<intArrays[minIndex]){
//如果這一趟有最小值,則儲存它的下標。如果這一趟沒有最小值,這下標還是這趟的第一個數字
minIndex=j;
}
}
//將這趟的第一個數字和這趟的最小值交換位置
NumberUtils.exchange(intArrays,i,minIndex);
}
}
/**
* 執行入口,intArrays:待排序的陣列,displaySort:是否顯示排序前和排序後的內容。
*/
public static void run(int intArrays[],boolean displaySort){
//克隆一份陣列
int arrays[]= intArrays.clone();
// 判斷是否需要顯示排序前的內容
if(displaySort){
NumberUtils.display(arrays,1);
}
// 記錄開始時間
long startTime=System.currentTimeMillis();
sort(arrays);
// 記錄結束時間
long endTime=System.currentTimeMillis();
// 判斷是否需要顯示排序前的內容
if(displaySort){
NumberUtils.display(arrays,2);
}
System.out.println("選擇排序用時:"+(endTime-startTime)+"毫秒");
}
/**
* 測試排序用的主方法
*/
public static void main(String[] args){
//陣列長度
int length=30000;
//最大值
int max =100000000;
//是否列印排序後的內容
boolean display=true;
//隨機獲取的排序陣列
int intArrays[]= NumberUtils.getRandomArs(length,max);
//選擇排序
Selection.run(intArrays,display);
}
}
執行main方法,終端輸出
排序前:36256124 88245266 79330698 29894836 61606201 15293028 79866040 83062619 56056393 ......91896916 65206360 93030472 15057437 67731366 10136030 26583447 91575080 26075174 23165997
排序後:1504 2678 16862 22268 25332 27669 28592 30945 32215 ......99960873 99961329 99962375 99983391 99984152 99985731 99986770 99987019 99987508 99991586
選擇排序用時:760毫秒
堆排序
什麼是堆?
堆是一棵順序儲存的完全二叉樹。
小根堆:每個結點的關鍵字都不大於其孩子結點的關鍵字。
大根堆:每個結點的關鍵字都不小於其孩子結點的關鍵字。
對於n個元素的序列{R0, R1, … , Rn}當且僅當滿足下列關係之一時,稱之為堆:
(1) Ri <= R2i+1 且 Ri <= R2i+2 (小根堆)
(2) Ri >= R2i+1 且 Ri >= R2i+2 (大根堆)
利用大頂堆(小頂堆)堆頂記錄的是最大關鍵字(最小關鍵字)這一特性,使得每次從無序中選擇最大記錄(最小記錄)變得簡單。
其基本思想為(大頂堆):
1)將初始待排序關鍵字序列(R1,R2….Rn)構建成大頂堆,此堆為初始的無序區;
2)將堆頂元素R[1]與最後一個元素R[n]交換,此時得到新的無序區(R1,R2,……Rn-1)和新的有序區(Rn),且滿足R[1,2…n-1]<=R[n];
3)由於交換後新的堆頂R[1]可能違反堆的性質,因此需要對當前無序區(R1,R2,……Rn-1)調整為新堆,然後再次將R[1]與無序區最後一個元素交換,得到新的無序區(R1,R2….Rn-2)和新的有序區(Rn-1,Rn)。不斷重複此過程直到有序區的元素個數為n-1,則整個排序過程完成。
構建初始堆
整體排序流程
堆排序的java程式碼
public class Heap {
/**
* 交換第一個和最後一個元素,輸出最後一個元素(最大值),然後把剩下元素重新調整為大根堆
*/
public static void sort(int[] array, int parent, int length) {
int temp = array[parent]; // temp儲存當前父節點
int child = 2 * parent + 1; // 先獲得左孩子
while (child < length) {
// 如果有右孩子結點,並且右孩子結點的值大於左孩子結點,則選取右孩子結點
if (child + 1 < length && array[child] < array[child + 1]) {
child++;
}
// 如果父結點的值已經大於孩子結點的值,則直接結束
if (temp >= array[child]){
break;
}
// 把孩子結點的值賦給父結點
array[parent] = array[child];
// 選取孩子結點的左孩子結點,繼續向下篩選
parent = child;
child = 2 * child + 1;
}
array[parent] = temp;
}
/**
* 堆排序開始入口
*/
public static void sort(int[] list) {
// 迴圈建立初始堆
for (int i = list.length / 2; i >= 0; i--) {
sort(list, i, list.length);
}
// 進行n-1次迴圈,完成排序
for (int i = list.length - 1; i > 0; i--) {
// 最後一個元素和第一元素進行交換
int temp = list[i];
list[i] = list[0];
list[0] = temp;
// 篩選 R[0] 結點,得到i-1個結點的堆
sort(list, 0, i);
}
}
/**
* 執行入口,intArrays:待排序的陣列,displaySort:是否顯示排序前和排序後的內容。
* @Author linyuanhuang
*/
public static void run(int intArrays[],boolean displaySort){
//克隆一份陣列
int arrays[]= intArrays.clone();
// 判斷是否需要顯示排序前的內容
if(displaySort){
NumberUtils.display(arrays,1);
}
// 記錄開始時間
long startTime=System.currentTimeMillis();
sort(arrays);
// 記錄結束時間
long endTime=System.currentTimeMillis();
// 判斷是否需要顯示排序前的內容
if(displaySort){
NumberUtils.display(arrays,2);
}
System.out.println("堆排序用時:"+(endTime-startTime)+"毫秒");
}
/**
* 測試排序用的主方法
*/
public static void main(String[] args){
//陣列長度
int length=30000;
//最大值
int max =100000000;
//是否列印排序後的內容
boolean display=true;
//隨機獲取的排序陣列
int intArrays[]= NumberUtils.getRandomArs(length,max);
//堆排序
Heap.run(intArrays,display);
}
}
執行main()方法,終端輸出
排序前:68501387 84285957 90673930 28384601 15869847 64540413 10108132 30787337 38015080 ......12618940 19612007 61253848 71654829 94083159 68570180 32741260 91442376 31312672 25760202
排序後:1636 5777 9397 18958 24136 24369 25759 29418 29597 ......99950746 99967732 99973624 99975571 99978358 99979889 99989824 99991114 99994023 99997694
堆排序用時:16毫秒
交換排序
氣泡排序
它適合資料規模很小的時候,而且它的效率也比較低,但是作為入門的排序演算法,還是值得學習的。
什麼是氣泡排序?
顧名思義,像水裡吐的泡泡一樣,因為水越深壓強越大,而泡泡的在水裡的由深變淺。所以,同樣的氣體體積,第一個出來的泡泡比第二個出來的要大。如下圖所示
氣泡排序的Java程式碼
public class Bubble {
/**
* 氣泡排序演算法方法,intArrays為傳入的陣列
*/
public static void sort(int[] intArrays){
int length=intArrays.length-1;
for(int i=0;i<length;i++){
//每一次迴圈找出最大值
for(int j=0;j<length-i;j++){
if(intArrays[j]>intArrays[j+1]){
//如果前面的數比後面的數大就交換它們的位置
NumberUtils.exchange(intArrays,j,j+1);
}
}
}
}
/**
* 執行入口,intArrays:待排序的陣列,displaySort:是否顯示排序前和排序後的內容。
*/
public static void run(int intArrays[],boolean displaySort){
//克隆一份陣列
int arrays[]= intArrays.clone();
// 判斷是否需要顯示排序前的內容
if(displaySort){
NumberUtils.display(arrays,1);
}
// 記錄開始時間
long startTime=System.currentTimeMillis();
sort(arrays);
// 記錄結束時間
long endTime=System.currentTimeMillis();
// 判斷是否需要顯示排序前的內容
if(displaySort){
NumberUtils.display(arrays,2);
}
System.out.println("氣泡排序用時:"+(endTime-startTime)+"毫秒");
}
/**
* 測試排序用的主方法
*/
public static void main(String[] args){
//陣列長度
int length=30000;
//最大值
int max =100000000;
//是否列印排序後的內容
boolean display=false;
//隨機獲取的排序陣列
int intArrays[]= NumberUtils.getRandomArs(length,max);
Bubble.run(intArrays,display);
}
}
執行main()方法,終端輸出
排序前:98692885 78734154 7745406 97043438 68720323 42819146 8210211 27611617 78857452 ......51820813 58031010 85028575 47959133 49404805 84102205 12103474 46209285 79427548 12704778
排序後:3621 6462 7920 11753 15464 17636 19331 23490 31389 ......99954272 99955130 99969666 99970755 99974592 99975509 99981138 99988660 99993917 99996791
氣泡排序用時:2327毫秒
快速排序
設要排序的陣列是A[0]……A[N-1],首先任意選取一個數據(通常選用陣列的第一個數)作為關鍵資料,然後將所有比它小的數都放到它前面,所有比它大的數都放到它後面,這個過程稱為一趟快速排序。值得注意的是,快速排序不是一種穩定的排序演算法,也就是說,多個相同的值的相對位置也許會在演算法結束時產生變動。
排序流程
1.首先哨兵j從右開始找比基位6小的數,注意這裡一定是哨兵j先開始走,因為和基位交換的是哨兵i。如果哨兵j一直沒有找到比基位6小的數就會和哨兵i相遇。說明哨兵i所在的位置基位(因為哨兵i還沒開始走)是最小值基位6。直到哨兵j找到比基位小的數,哨兵i才開始從左開始找比基位6大的數。
2.哨兵j從右開始找比基位6小的數“5”,哨兵i比基位6大的數“7”
3.交換“5”跟“7”的位置
4.繼續重複1-3的步驟
5.哨兵j繼續往左走,找到了比基位6小的數“3”後停止。然後哨兵i往右走找比基位6大的數。正好哨兵i和哨兵j相遇,所有的數都已經和6比較完了。這時候哨兵i的位置和基位“6”交換。
6.交換完後“6”左邊的都比“6”小,“6”右邊的都比“6”大
7.繼續按上面的步驟比較“6”左邊的3,1,2,5,4和右邊的9,7,10,8直到所有的數都為有序狀態
快速排序的Java程式碼
public class Quick {
private static void sort(int[] intArrays ,int left,int right) {
//如果左索引大於右索引,直接返回
if(left > right){
return;
}
int i = left ;
int j = right;
int temp = intArrays[left];//設定基準值,將最左端元素作為基準值
while(i != j){
//往左移位,直到小於temp
while(i<j && intArrays[j]>=temp){
j--;
}
//往右移位,直到大於temp
while(i<j && intArrays[i]<=temp){
i++;
}
if(i < j){
//如果i<j,也就是說i和j還沒相遇時,交換彼此的資料
NumberUtils.exchange(intArrays,i,j);
}
}
//當哨兵i與哨兵j相遇時退出迴圈,將哨兵i與基位交換位置
NumberUtils.exchange(intArrays,left,i);
//下一次迭代
sort(intArrays,left,i-1);//左半邊
sort(intArrays,j+1,right);//右半邊
}
/**
* 執行入口,intArrays:待排序的陣列,displaySort:是否顯示排序前和排序後的內容。
* @author linyuanhuang
* @Date 11:50 2017/12/28
* @return void
*/
public static void run(int intArrays[],boolean displaySort){
//克隆一份陣列
int arrays[]= intArrays.clone();
// 判斷是否需要顯示排序前的內容
if(displaySort){
NumberUtils.display(arrays,1);
}
// 記錄開始時間
long startTime=System.currentTimeMillis();
sort(arrays,0,arrays.length-1);
// 記錄結束時間
long endTime=System.currentTimeMillis();
// 判斷是否需要顯示排序前的內容
if(displaySort){
NumberUtils.display(arrays,2);
}
System.out.println("快速排序用時:"+(endTime-startTime)+"毫秒");
}
/**
* 測試排序用的主方法
*/
public static void main(String[] args){
//陣列長度
int length=30000;
//最大值
int max =100000000;
//是否列印排序後的內容
boolean display=true;
//隨機獲取的排序陣列
int intArrays[]= NumberUtils.getRandomArs(length,max);
//快速排序
Quick.run(intArrays,display);
}
}
執行main方法,終端輸出
排序前:43139629 7649925 81580157 40853496 33416709 66768707 92323968 94560370 19094255 ......84317262 13986402 3498445 28911241 28768513 583418 73503650 17522821 70353692 97132323
排序後:5619 5823 15600 20273 24455 26206 27629 28694 31586 ......99964856 99970127 99983058 99984222 99989168 99990288 99992909 99995121 99999366 99999942
快速排序用時:9毫秒
歸併排序
歸(遞迴)並(合併)排序採用了分治策略(divide-and-conquer),就是將原問題分解為一些規模較小的相似子問題,然後遞迴解決這些子問題,最後合併其結果作為原問題的解。
歸併排序將待排序陣列A[1..n]分成兩個各含n/2個元素的子序列,然後對這個兩個子序列進行遞迴排序,最後將這兩個已排序的子序列進行合併,即得到最終排好序的序列。具體排序過程如下圖所示:
歸併排序的Java程式碼
public class Merge {
/**
* 臨時陣列空間
*/
private static int[] tmpArray;
/**
* 排序--分解為一些規模較小
* @author linyuanhuang
* @Date 12:01 2017/12/28
* @return void
*/
private static void sort(int[] a, int left, int mid, int right) {
int i = left; //左陣列下一個要進行比較的元素的索引
int j = mid + 1; //右陣列下一個要進行比較的元素的索引
int N = right + 1; //本次歸併的元素數目
for (int k = left; k <= right; k++) {
if (i > mid) { //左陣列元素已全比較完
tmpArray[k] = a[j++];
} else if (j > right) { //右陣列元素已全比較完
tmpArray[k] = a[i++];
} else if (a[j] < a[i]) { //右陣列元素小於左陣列
tmpArray[k] = a[j++];
} else { //右陣列元素大於等於左陣列
tmpArray[k] = a[i++];
}
}
//歸併完成後,再複製回原陣列
for (int k = left; k < N; k++) {
a[k] = tmpArray[k];
}
}
/**
* 歸併排序開始入口
* @author linyuanhuang
* @Date 11:57 2017/12/28
* @return void
*/
public static void sort(int[] a) {
int N = a.length;
tmpArray = new int[N+1]; //用於暫時存放比較後的元素
merge(a, 0, N - 1);
}
/**
* 遞迴方法
* @author linyuanhuang
* @Date 12:04 2017/12/28
* @return void
*/
private static void merge(int[] a, int left, int right) {
//左索引大於等於右索引直接返回
if (left >= right) {
return;
}
//一分為二
int mid = (left + right) / 2;
//遞迴一分為二左邊的佇列
merge(a, left, mid);
//遞迴一分為二右邊的佇列
merge(a, mid+1, right);
//排序
sort(a, left, mid, right);
}
/**
* 執行入口,intArrays:待排序的陣列,displaySort:是否顯示排序前和排序後的內容。
* @Author linyuanhuang
* @Date 2017/12/22 15:11
* @return void
*/
public static void run(int intArrays[],boolean displaySort){
//克隆一份陣列
int arrays[]= intArrays.clone();
// 判斷是否需要顯示排序前的內容
if(displaySort){
NumberUtils.display(arrays,1);
}
// 記錄開始時間
long startTime=System.currentTimeMillis();
sort(arrays);
// 記錄結束時間
long endTime=System.currentTimeMillis();
// 判斷是否需要顯示排序前的內容
if(displaySort){
NumberUtils.display(arrays,2);
}
System.out.println("歸併排序用時:"+(endTime-startTime)+"毫秒");
}
}
/**
* 測試排序用的主方法
*/
public static void main(String[] args){
//陣列長度
int length=30000;
//最大值
int max =100000000;
//是否列印排序後的內容
boolean display=true;
//隨機獲取的排序陣列
int intArrays[]= NumberUtils.getRandomArs(length,max);
//歸併
Merge.run(intArrays,display);
}
}
執行main方法,終端輸出
排序前:57743991 26662091 86541142 1246814 1386809 51656969 28286793 30436571 97487893 ......20682423 59991342 19182409 51239835 5409807 47490219 76499337 42071620 8118816 30246698
排序後:3676 4222 4278 5117 5200 5657 6917 7896 9204 ......99987402 99989259 99989512 99989679 99992547 99993518 99995210 99996405 99998701 99999234
歸併排序用時:499毫秒
基數排序
基數排序(radix sort)又稱桶排序(bucket sort),相對於常見的比較排序,基數排序是一種分配式排序,即通過將所有數字分配到應在的位置最後再覆蓋到原陣列完成排序的過程。它是一種穩定的排序演算法,但有一定的侷限性:
1、關鍵字可分解;
2、記錄的關鍵字位數較少,如果密集更好;
3、如果是數字時,最好是無符號的,否則將增加相應的映射覆雜度,可先將其正負分開排序。
初始化:構造一個10*n的二維陣列,一個長度為n的陣列用於儲存每次位排序時每個桶子裡有多少個元素。
迴圈操作:從低位開始(我們採用LSD的方式),將所有元素對應該位的數字存到相應的桶子裡去(對應二維