常见14种经典排序算法(Java代码实现)

常见14种经典排序算法(Java代码实现)

Posted by 山大王 on December 12, 2018

“一年之计在于春,一日之计在于晨。一家之计在于和,一生之计在于勤。”

正文

一,冒泡排序

排序算法其实有很多,冒泡排序基本上算是最简单的一种排序算法了。他的原理就和他的名字一样,通过不断的比较把小的数据不断的往前移。其实冒泡排序有很多变种,我们会一一来看。我们先看下最常见的一种代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void bubbleSort1(int array[]) {
    int length = array.length;
    for (int i = 0; i < length - 1; i++) {
        for (int j = i + 1; j < length; j++) {
            if (array[j] < array[i]) {
                swap(array, i, j);
            }
        }
    }
}
                                             
public static void swap(int[] A, int i, int j) {
    if (i != j) {
        A[i] ^= A[j];
        A[j] ^= A[i];
        A[i] ^= A[j];
    }
}

上面排序的原理相当于气泡从水中往上冒,会逐渐变大。首先拿第一个元素和后面的所有一个个比较,如果比后面的大就交换,所以始终会保证第一个元素是最小的,然后再从第二个第三个,以此类推,swap方法表示交换两个数字的值。上面排序是每次都把待排数组中最小的值放到前面,我们能不能每次都把待排数组中最大的值放到后面呢,其实这种也是可以的,看下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void bubbleSort2(int array[]) {
    int length = array.length;
    for (int i = length - 1; i >= 0; i--) {
        for (int j = i - 1; j >= 0; j--) {
            if (array[j] > array[i]) {
                swap(array, i, j);
            }
        }
    }
}
                                           
public static void swap(int[] A, int i, int j) {
    if (i != j) {
        A[i] ^= A[j];
        A[j] ^= A[i];
        A[i] ^= A[j];
    }
}

这种排序和上面第一种的那个排序正好相反,它的原理相当于气泡从大到小往水中钻,越往上气泡越大,越往下气泡越小。我们还可以前后相邻的两两比较,每一轮比较完了之后其实也会把最大的放到后面,我们看下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void bubbleSort3(int array[]) {
    int length = array.length;
    for (int i = 0; i < length - 1; i++) {
        for (int j = 1; j < length - i; j++) {
            if (array[j] < array[j - 1]) {
                swap(array, j, j - 1);
            }
        }
    }
}
                                     
public static void swap(int[] A, int i, int j) {
    if (i != j) {
        A[i] ^= A[j];
        A[j] ^= A[i];
        A[i] ^= A[j];
    }
}

如果数组本来就是排序好的,或者后面的都已经排序好了,我们在一个个循环其实效率并不是很高。在排序的时候如果我们发现后面待排数组已经排序好了,后面的我们就不用一个个再排序了,我们可以改进一下,看一下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void bubbleSort4(int array[]) {
    int length = array.length;
    boolean flag;
    for (int i = 0; i < length - 1; i++) {
        flag = true;
        for (int j = 1; j < length - i; j++) {
            if (array[j] < array[j - 1]) {
                swap(array, j, j - 1);
                flag = false;
            }
        }
        if (flag) break;
    }
}
                                     
public static void swap(int[] A, int i, int j) {
    if (i != j) {
        A[i] ^= A[j];
        A[j] ^= A[i];
        A[i] ^= A[j];
    }
}

如果不喜欢for循环,我们也可以改为while循环,看下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void bubbleSort5(int array[]) {
    int length = array.length;
    int i = 0, j;
    while (i < length - 1) {
        j = i + 1;
        while (j < length) {
            if (array[j] < array[i]) {
                swap(array, i, j);
            }
            j++;
        }
        i++;
    }
}
                                     
public static void swap(int[] A, int i, int j) {
    if (i != j) {
        A[i] ^= A[j];
        A[j] ^= A[i];
        A[i] ^= A[j];
    }
}

如果喜欢递归我们也可以改为递归的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void bubbleSort6(int[] array, int n) {
    if (n == 1)
        return;
    if (array == null || array.length == 0)
        return;
    //逐渐减少n,每次都是把最大的放在最后面,直到n为1
    for (int i = 0; i < n - 1; i++) {
        if (array[i] > array[i + 1]) {
            swap(array, i, i + 1);
        }
    }
    bubbleSort6(array, n - 1);
}
                                     
public static void swap(int[] A, int i, int j) {
    if (i != j) {
        A[i] ^= A[j];
        A[j] ^= A[i];
        A[i] ^= A[j];
    }
}

二,选择排序

选择排序是默认前面都是已经排序好的,然后从后面选择最小的放在前面排序好的的后面,首先第一轮循环的时候默认的排序好的为空,然后从后面选择最小的放到数组的第一个位置,第二轮循环的时候默认第一个元素是已经排序好的,然后从剩下的找出最小的放到数组的第二个位置,第三轮循环的时候默认前两个都是已经排序好的,然后再从剩下的选择一个最小的放到数组的第三个位置,以此类推,直到所有数据都遍历完为止。我们看一下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static void selectSort1(int[] array) {
    for (int i = 0; i < array.length - 1; i++) {
        int index = i;
        for (int j = i + 1; j < array.length; j++) {
            if (array[index] > array[j]) {
                index = j;
            }
        }
        if (i != index) {
            swap(array, i, index);
        }
    }
}
                                      
public static void swap(int[] A, int i, int j) {
    A[i] = A[i] + A[j];
    A[j] = A[i] - A[j];
    A[i] = A[i] - A[j];
}

我们看到,他和冒泡排序很像,但不同的是每次遇到需要交换的时候并没有立即交换,而是记录下他的index,等这一轮循环完了之后在交换,选择排序很简单,基本上没什么可说的,我们来看一下他的递归该怎么实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//调用方式selectSort2(array, 0);
private static void selectSort2(int array[], int index) {
    if (index == array.length)
        return;
    int min = index;
    for (int i = index + 1; i < array.length; i++) {
        if (array[i] < array[min]) min = i;
    }
    if (min != index) {
        swap(array, index, min);
    }
    selectSort2(array, ++index);
}
                                    
public static void swap(int[] A, int i, int j) {
    A[i] = A[i] + A[j];
    A[j] = A[i] - A[j];
    A[i] = A[i] - A[j];
}

三,插入排序

插入排序的原理是默认前面的元素都是已经排序好的,然后从后面逐个读取插入到前面排序好的合适的位置,就相当于打扑克的时候每获取一张牌的时候就插入到合适的位置一样。插入排序可以分为两种,一种是直接插入还一种是二分法插入,直接插入的原理比较简单,就是往前逐个查找直到找到合适的位置然后插入,第一次循环的时候相当于第一个元素是排序好的,然后第二个元素和第一个元素对比,确定是插入他的前面还是他的后面……,第n次循环的时候相当于前面n个元素都是排序好的,第n+1个元素在和前面的n个比较,找到合适的位置插入。二分法插入是先折半查找,找到合适的位置然后再插入。我们先看一下简单的直接插入排序代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static void insertSort1(int[] array) {
    for (int i = 1; i < array.length; i++) {
        int j;
        int temp = array[i];
        for (j = i - 1; j >= 0; j--) {
            if (array[j] > temp) {
                array[j + 1] = array[j];//往后挪
            } else {//如果前面没有比他大则break
                break;
            }
        }
        array[j + 1] = temp;
    }
}

把后面的一个个插入到前面合适的位置,如果不喜欢for循环也可以改为while循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static void insertSort2(int[] array) {
    int length = array.length;
    int i = 0;
    while (i < length) {
        int j = i;
        int key = array[i];
        while (j > 0 && array[j - 1] > key) {
            array[j] = array[j - 1];
            j--;
        }
        if (i != j) {
            array[j] = key;
        }
        i++;
    }
}

如果数据很大的时候,到后面还要在一个个往前找合适的位置其实效率并不高,因为插入排序的原理就是相当于前面的数据都已经排序好了,然后从下一个开始找到前面合适的位置插入,既然我们已经知道前面的都是排序好的了,在一个个找其实很费时,我们完全可以使用二分法查找来找到合适的位置然后再插入,关于二分法查找在下一章会有介绍。我们先看一下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private static void insertSort3(int[] array) {
    int length = array.length;
    for (int i = 1; i < length; i++) {
        if (array[i - 1] > array[i]) {
            int key = array[i];
            int low = 0;
            int hight = i - 1;
            while (low <= hight) {
                int mid = (low + hight) >> 1;
                if (array[mid] > key) {
                    hight = mid - 1;
                } else {
                    low = mid + 1;
                }
            }
            for (int j = i; j > low; j--) {
                array[j] = array[j - 1];
            }
            array[low] = key;
        }
    }
}

如果看不明白没关系,下一章会介绍二分法查找。我们来看一下使用递归该怎么写

1
2
3
4
5
6
7
8
9
10
11
private static void insertSort4(int[] data, int n) {
    if (n < 2) return;
    insertSort4(data, --n);//相当于前面n-1个都已经排序好了
    int temp = data[n];//把最后一个元素插入到合适的位置
    int index = n - 1;
    while (index >= 0 && data[index] > temp) {
        data[index + 1] = data[index];
        index--;
    }
    data[index + 1] = temp;
}

四,快速排序

前面3种排序都非常简单,也很好理解,前面在讲到冒泡排序的时候说过冒泡排序是有很多变种,其实快速排序也有很多变种。快速排序原理是首先要找到一个中枢,把小于中枢的值放到他前面,大于中枢的值放到他的右边,然后再以此方法对这前后两部分数据再分别进行快速,一直排序下去,直到不可再划分为止,我们看下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static void quickSort1(int[] array, int start, int end) {
    if (start < end) {
        int key = array[start];//用待排数组的第一个作为中枢
        int i = start;
        for (int j = start + 1; j <= end; j++) {
            if (key > array[j]) {
                swap(array, j, ++i);
            }
        }
        array[start] = array[i];//先挪,然后再把中枢放到指定位置
        array[i] = key;
        quickSort1(array, start, i - 1);
        quickSort1(array, i + 1, end);
    }
}
                               
private static void swap(int[] A, int i, int j) {
    if (i != j) {
        A[i] ^= A[j];
        A[j] ^= A[i];
        A[i] ^= A[j];
    }
}

他的原理就是先找一个中枢值,默认我们选择第一个作为中枢值,然后对数组进行遍历,比中枢值小的放到前面,比中枢值大的放到后面,第一轮循环完后再把中枢值插入到这两部分的中间,所以第一轮循环完之后的结果就是中枢值前面的数都比他小,后面的数都比他大。然后在对中枢值前面部分和中枢值后面部分在分别采用这种方法,我们来看一张图加深一下理解

我们上面说过快速排序有很多变种,上面的方式就是中枢值先不变,待第一轮排完之后再把中枢值放到合适的位置。下面再来看另外一种方式,就是中枢值在比较的时候始终是变的,一会在前面一会在后面,当第一轮执行完之后,大于中枢值的都会放到他的后面,小于中枢值的都会放到他的前面,我们来看一下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
private static void quickSort2(int[] array, int start, int end) {
    if (start < end) {
        int pivot = partition(array, start, end);
        quickSort2(array, start, pivot - 1);
        quickSort2(array, pivot + 1, end);
    }
}
                           
private static int partition(int[] array, int start, int end) {
    int pivot = start;
    while (start != end) {
        if (pivot != end) {
            /**
             * 第一次循环的时候用第一个元素作为中枢,和最后一个进行对比,
             * 如果小于最后一个元素,执行end--然后和倒数第二个元素进行
             * 对比,如果还小于继续执行end--……。如果大于最后一个元素那
             * 么就和最后一个元素交换,然后让中枢值pivot指向最后一个元素,
             * 下一轮循环的时候会指向下面的else方法,然后和前面的元素进行
             * 比较,如果前面没有大于他的则执行start++,如果有大于他的则
             * 和前面的交换。也即是说这个中枢的位置 始终是在变动的,一会在
             * 和面一会在前面。
             * 比如数组{5,2,6,3,8}
             * 第一轮循环的时候:首先5和最后第一个数8比较,由于5小于8所以
             * 中枢值5不动,然后5继续和最后第二个数3比较,由于比3大,所以
             * 中枢值5和3交换,变为{3,2,6,5,8},然后中枢值5在和前面的2
             * 比较,由于5大于2,所以5不动,然后5在和6比较,由于小于6,所以
             * 中枢值5和6交换,交换结果为{3,2,5,6,8},所以第一轮执行完之后
             * 大于中枢值5的数都放到了他的后面,小于中枢值5的数都放到了他的
             * 前面,然后对前面和后面部分再分别使用此方法。
             */
            if (array[end] < array[pivot]) {
                swap(array, end, pivot);
                pivot = end;
            } else {
                end--;
            }
        } else {
            if (array[start] > array[pivot]) {
                swap(array, start, pivot);
                pivot = start;
            } else {
                start++;
            }
        }
    }
    return pivot;
}

如果还看不明白,我们可以换种方法,估计下面这种代码更好理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private static void quickSort3(int[] array, int start, int end) {
    if (start < end) {
        int pivot = partition3(array, start, end);
        quickSort3(array, start, pivot - 1);
        quickSort3(array, pivot + 1, end);
    }
}
                           
private static int partition3(int[] array, int start, int end) {
    int pivot = array[start];// 采用子序列的第一个元素作为枢纽元素
    while (start < end) {//
        // 从后往前在后半部分中寻找第一个小于枢纽元素的元素
        while (start < end && array[end] >= pivot) {
            --end;
        }
        // 将这个比枢纽元素小的元素交换到前半部分
        swap(array, start, end);
        // 从前往后在前半部分中寻找第一个大于枢纽元素的元素
        while (start < end && array[start] <= pivot) {
            ++start;
        }
        swap(array, start, end);// 将这个枢纽元素大的元素交换到后半部分
    }
    return start;// 返回枢纽元素所在的位置
}

我们上面分析了快速排序的两种方式,一种数中枢值不动,把小于中枢值的往前挪,大于中枢值的往后挪,最后再把中枢值放到指定的位置。另一种方法是中枢值始终是变动的,一会和后面的比较一会和前面的比较。我们下面再来看一种方式是上面两种方式的结合,他是中枢值先不动,后面的数据前后两两比较,排完之后再把中枢值放到合适的位置,我们看下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static void quickSort4(int[] array, int start, int end) {
    if (start < end) {
        int pivot = partition4(array, start, end);
        quickSort4(array, start, pivot - 1);
        quickSort4(array, pivot + 1, end);
    }
}
                          
private static int partition4(int[] array, int start, int end) {
    int pivot = start;
    start++;
    while (start != end) {
        while (start < end && array[start] < array[pivot]) {
            start++;
        }
        while (start < end && array[end] >= array[pivot]) {
            end--;
        }
        swap(array, start, end);
    }
    swap(array, --start, pivot);
    pivot = start;
    return pivot;
}

上面的快速排序我们都使用递归的方法,我们下面再来看一下非递归该怎么写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private static void quickSort5(int[] a, int start, int end) {
    Stack<Integer> temp = new Stack();
    temp.push(end);
    temp.push(start);
    while (!temp.empty()) {
        int i = temp.pop();//start
        int j = temp.pop();//end
        int k = partition5(a, i, j);
        if (k > i) {
            temp.push(k - 1);//end
            temp.push(i);//start
        }
        if (j > k) {
            temp.push(j);//end
            temp.push(k + 1);//start
        }
    }
}
                         
private static int partition5(int[] array, int start, int end) {
    int pivot = array[start];// 采用子序列的第一个元素作为枢纽元素
    while (start < end) {//
        // 从后往前在后半部分中寻找第一个小于枢纽元素的元素
        while (start < end && array[end] >= pivot) {
            --end;
        }
        // 将这个比枢纽元素小的元素交换到前半部分
        swap(array, start, end);
        // 从前往后在前半部分中寻找第一个大于枢纽元素的元素
        while (start < end && array[start] <= pivot) {
            ++start;
        }
        swap(array, start, end);// 将这个枢纽元素大的元素交换到后半部分
    }
    return start;// 返回枢纽元素所在的位置
}

五,归并排序

要明白归并排序,必须搞清楚递归的原理。归并排序是将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。他采用的一种分治的算法,先分然后在合并,我们看一下原理图

我们来看一下归并排序的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private static void mergeSort1(int array[], int left, int right) {
    if (left < right) {
        int center = (left + right) >> 1;
        mergeSort1(array, left, center);
        mergeSort1(array, center + 1, right);
        merge1(array, left, center, right);
    }
}
                        
private static void merge1(int[] data, int left, int center, int right) {
    int[] tmp = new int[data.length];
    int tempIndex = left;
    //_left是前半部分开始的位置,_right是后半部分开始的位置
    int _left = left;
    int _right = center + 1;
    while (_left <= center && _right <= right) {
        if (data[_left] <= data[_right]) {
            tmp[tempIndex++] = data[_left++];
        } else {
            tmp[tempIndex++] = data[_right++];
        }
    }
    while (_right <= right) {
        tmp[tempIndex++] = data[_right++];
    }
    while (_left <= center) {
        tmp[tempIndex++] = data[_left++];
    }
    while (left <= right) {
        data[left] = tmp[left++];
    }
}

他是先分然后在合并,合并的时候就相当于把两个有序数组合并为一个有序数组。我们看到上面代码在合并的时候申请了一个临时数组,每次合并的时候都要申请一个临时数组,这样很浪费空间,其实我们只需要申请我们够我们使用的就行了,不用申请太大的数组,我们改一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private static void mergeSort2(int array[], int left, int right) {
    if (left < right) {
        int center = (left + right) >> 1;
        mergeSort2(array, left, center);
        mergeSort2(array, center + 1, right);
        merge2(array, left, center, right);
    }
}
                       
public static void merge2(int[] data, int left, int center, int right) {
    int length = right - left + 1;
    //只需要申请够我们使用的就行了
    int[] tmp = new int[length];
    int tempIndex = 0;
    //_left是前半部分开始的位置,_right是后半部分开始的位置
    int _left = left;
    int _right = center + 1;
    while (_left <= center && _right <= right) {
        if (data[_left] <= data[_right]) {
            tmp[tempIndex++] = data[_left++];
        } else {
            tmp[tempIndex++] = data[_right++];
        }
    }
    while (_right <= right) {
        tmp[tempIndex++] = data[_right++];
    }
    while (_left <= center) {
        tmp[tempIndex++] = data[_left++];
    }
    tempIndex = 0;
    while (tempIndex < length) {
        data[left + tempIndex] = tmp[tempIndex++];
    }
}

上面的代码都使用了递归,先分开在合并,我们来看下使用非递归来怎么写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public static void mergeSort3(int[] data) {
    int i = 1;
    while (i < data.length) {
        //原理很简单,就是先两个两个合并,然后4个,然后8个……
        for (int j = 0; j + i < data.length; j += 2 * i) {
            merge3(data, j, j + i - 1, Math.min(j + 2 * i - 1, data.length - 1));
        }
        i = i << 1;
    }
}
                      
public static void merge3(int[] data, int left, int center, int right) {
    int length = right - left + 1;
    int[] tmp = new int[length];
    int tempIndex = 0;
    //_left是前半部分开始的位置,_right是后半部分开始的位置
    int _left = left;
    int _right = center + 1;
    while (_left <= center && _right <= right) {
        if (data[_left] <= data[_right]) {
            tmp[tempIndex++] = data[_left++];
        } else {
            tmp[tempIndex++] = data[_right++];
        }
    }
    while (_right <= right) {
        tmp[tempIndex++] = data[_right++];
    }
    while (_left <= center) {
        tmp[tempIndex++] = data[_left++];
    }
    tempIndex = 0;
    while (tempIndex < length) {
        data[left + tempIndex] = tmp[tempIndex++];
    }
}

六,堆排序

堆排序也可以理解为二叉树排序,这里的堆分为两种,一种是大顶堆,一种是小顶堆,大顶堆就是根节点不小于他的两个子节点,小顶堆就是根节点不大于他的两个子节点,排序之前我们先要构建堆(也就是二叉树),然后再从堆中一个个取数据,每次取完数据都要对堆进行一步调整,我们看一个图来加深一下理解,先来看一下堆的构建过程

我们构建的是大顶堆,就是父节点不小于他的子节点(如果有子节点),堆构建完之后我们就可以排序了,因为我们构建的是大顶堆,所以根节点总是最大的,所以我们每次都把根节点拿走,然后再把最后一个节点放到根节点,但这样又打破了堆的平衡,所以要往下调整……,重复上面步骤,直到堆为空位置。我们根据一张图来看下堆排序的过程。

堆的排序就是每次把堆顶的元素拿走,再把最后一个元素放到堆顶,然后再往下调整。我们来看一下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
private static void heapSort(int[] array) {
    int length = array.length;
    buildMaxHeap(array, length);
    for (int i = 0; i < length; i++) {
        swap(array, 0, length - 1 - i);
        maxHeapfy(array, 0, length - i - 1);
    }
}
                    
private static void maxHeapfy(int[] array, int i, int heapSize) {
    int left = i * 2 + 1;
    int right = i * 2 + 2;
    int largest = i;
    if (left < heapSize && array[left] > array[largest]) {
        largest = left;
    }
    if (right < heapSize && array[right] > array[largest]) {
        largest = right;
    }
    if (largest != i) {//把最大值给父节点
        swap(array, largest, i);
        maxHeapfy(array, largest, heapSize);
    }
}
                    
private static void buildMaxHeap(int[] array, int heapSize) {
    //从最后一个非叶子节点开始循环
    for (int i = (heapSize - 2) >> 1; i >= 0; i--) {
        maxHeapfy(array, i, heapSize);
    }
}
                    
public static void swap(int[] A, int i, int j) {
    if (i != j) {
        A[i] ^= A[j];
        A[j] ^= A[i];
        A[i] ^= A[j];
    }
}

七,基数排序

基数排序的方式可以采用最低位优先LSD(Least sgnificant digital)法或最高位优先MSD(Most sgnificant digital)法,LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。我们这里使用LSD法,原理就是一个数组我们首先根据他的个位进行排序,然后在根据十位,百位……,这里最多排到多少位是根据他的最大值确定的,如果最大值有千位,我们必须要计算到千位,如果最多只有十位,我们就计算到十位就可以了,每一位都排序完了之后,数组也就排序成功了。假如我们有一组初始数据{2,16,97,113,56,211,789,8,0,29},我们使用一张图来看下是怎么排序的

他是先个位比较排序,然后再十位比较排序,然后再百位比较排序,等所有位都排序完了,整个数组也就排序完成了,我们看下代码该怎么写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
private static void radixSort1(int[] array) {
    int digitCount = 10;//从0到9最多10位数
    int maxCount = getBitCount1(getMaxNumbit1(array));
    int radix = 1;
    int[][] tempArray = new int[digitCount][array.length];
    for (int i = 0; i < maxCount; i++) {
        int[] count = new int[digitCount];
        for (int j = 0; j < array.length; j++) {
            int temp = ((array[j] / radix) % 10);
            tempArray[temp][count[temp]++] = array[j];
        }
        int index = 0;
        for (int j = 0; j < digitCount; j++) {
            if (count[j] == 0)
                continue;
            for (int k = 0; k < count[j]; k++) {
                array[index++] = tempArray[j][k];
            }
        }
        radix *= 10;
    }
}
                   
private static int getMaxNumbit1(int array[]) {
    int max = array[0];
    for (int i = 1, length = array.length; i < length; i++) {
        if (array[i] > max) {
            max = array[i];
        }
    }
    return max;
}
                   
private static int getBitCount1(int num) {
    int count = 1;
    int temp = num / 10;
    while (temp != 0) {
        count++;
        temp /= 10;
    }
    return count;
}

这就是基数排序,先排序每一位上的数字,等所有位上的数字都排序完了之后,整个数组的排序也就全部完成了。但是上面代码还不是很完美,因为当出现负数的时候上面代码就没法排序了,我们来想一下当出现负数的时候应该怎么办。如果出现负数的时候,我们可以让每一位上的数字求余之后再加上9,然后再放到临时数组中,因为求余的最大负数是-9,加上9之后,就不会出现负数了,我们来看下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
private static void radixSort2(int[] array) {
    int digitCount = 19;//从-9到9最多19位数
    int maxCount = getBitCount2(getMaxNumbit2(array));
    int radix = 1;
    int[][] tempArray = new int[digitCount][array.length];
    for (int i = 0; i < maxCount; i++) {
        int[] count = new int[digitCount];
        for (int j = 0; j < array.length; j++) {
            int temp = ((array[j] / radix) % 10) + 9;
            tempArray[temp][count[temp]++] = array[j];
        }
        int index = 0;
        for (int j = 0; j < digitCount; j++) {
            if (count[j] == 0)
                continue;
            for (int k = 0; k < count[j]; k++) {
                array[index++] = tempArray[j][k];
            }
        }
        radix *= 10;
    }
}
                  
private static int getMaxNumbit2(int array[]) {
    int max = array[0];
    int min = array[0];
    for (int i = 1, length = array.length; i < length; i++) {
        if (array[i] > max) {
            max = array[i];
        } else if (array[i] < min) {
            min = array[i];
        }
    }
    return max < -min ? -min : max;
}
                  
private static int getBitCount2(int num) {
    int count = 1;
    int temp = num / 10;
    while (temp != 0) {
        count++;
        temp /= 10;
    }
    return count;
}

我们来随便找一段代码测试一下结果是否正确

1
2
3
4
5
6
7
8
int[] array2 = new int[10];
Random random = new Random();
for (int i = 0; i < array2.length; i++) {
    array2[i] = random.nextInt(1000) - 500;
}
System.out.println("排序前:" + Arrays.toString(array2));
radixSort2(array2);
System.out.println("排序后:" + Arrays.toString(array2));

我们看一下运行结果

1
2
排序前[197, -75, -313, 474, 384, -202, -474, 401, 371, 163]
排序后[-474, -313, -202, -75, 163, 197, 371, 384, 401, 474]

八,桶排序

桶排序是将数组分散到有限的桶中,然后每个桶再分别排序,而每个桶的排序又可以使用其他排序方式进行排序,可以是桶排序也可以是其他排序。桶的大小可以随便定,如果桶的数量足够多就会变成我们后面介绍的计数排序,其实我们完全可以把桶固定在一个数量,根据数组的大小来确定,也可以自己定,比如3个或者5个7个等,桶的大小确定之后,下一步就需要把数组中的值一一存放到桶里,小的值就会放到前面的桶里,大的值就会放到后面的桶里,中间的值就会放到中间的桶里,然后再分别对每个桶进行单独排序,最后再把所有桶的数据都合并到一起就会得到排序好的数组,我们看下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private static void bucketSort1(int[] array, int bucketSize) {
    int arrayLength = array.length;
    int max = array[0];
    int min = array[0];
    for (int i = 0; i < arrayLength; i++) {
        if (array[i] > max)
            max = array[i];
        else if (array[i] < min)
            min = array[i];
    }
    //bucketSize表示每个桶存放数据的大小,bucketCount总共桶的数量
    int bucketCount = (max - min) / bucketSize + 1;
    List<List<Integer>> buckets = new ArrayList<>(bucketCount);
    for (int i = 0; i < bucketCount; i++) {
        buckets.add(new ArrayList<Integer>());
    }
               
    for (int i = 0; i < arrayLength; i++) {
        //根据value的大小存放到不同的桶里,最终的结果是小的出现在前面的桶里,
        //大的出现在后面的桶里吗,中间的也就在中间的桶里了,然后再对每个桶分别
        //进行排序。
        buckets.get((array[i] - min) / bucketSize).add(array[i]);
    }
               
    int currentIndex = 0;
    for (int i = 0; i < buckets.size(); i++) {
        //取出每个桶的数据
        Integer[] bucketArray = new Integer[buckets.get(i).size()];
        bucketArray = buckets.get(i).toArray(bucketArray);
        //每一个桶进行排序,这里面可以选择其他排序算法进行排序
        Arrays.sort(bucketArray);
        for (int j = 0; j < bucketArray.length; j++) {
            array[currentIndex++] = bucketArray[j];
        }
    }
}

这就是所谓的桶排序,首先要找到他的最大值和最小值,然后计算桶的数量,找出最小值是因为存放的时候要让当前值减去最小值,否则当排序中有负数的时候存放到桶里会报异常。

九,希尔排序

希尔排序也成缩小增量排序,原理是将待排序列划分为若干组,每组都是不连续的,有间隔step,step可以自己定,但间隔step最后的值一定是1,也就说最后一步是前后两两比较。间隔为step的默认划分为一组,先在每一组内进行排序,以使整个序列基本有序,然后再减小间隔step的值,重新分组再排序……不断重复,直到间隔step小于1则停止。我们通过一张图来看下希尔排序的原理。

上面的数组,假设第一轮增量是4,然后是每间隔4个为一组排序,第二轮的时候增量变为2,第三轮的时候增量变为1。直到增量为1的时候排序才算完成,我们看下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static void shellSort1(int[] array) {
    int length = array.length;
    int step = length >> 1;
    while (step >= 1) {
        for (int i = step; i < length; i++) {
            for (int j = i; j >= step; j -= step) {
                if (array[j] < array[j - step]) {
                    swap1(array, j, j - step);
                } else {
                    //如果大于,则不用继续往前比较了,
                    // 因为前面的元素已经排好序
                    break;
                }
            }
        }
        step >>= 1;
    }
}
              
public static void swap1(int[] A, int i, int j) {
    if (i != j) {
        A[i] ^= A[j];
        A[j] ^= A[i];
        A[i] ^= A[j];
    }
}

其实上面代码我们还可以优化一下,因为如果数量很大并且step又比较小的时候,两两比较交换显然不是很好。我们可以使用一个临时变量temp,首先把待比较的变量保存到temp中,然后往前找,如果前面的比他大,就会把前面的值挪到当前位置,然后再往前找,如果还比当前大那么还挪,直到循环完为止,然后再把当前保存的temp值放到最前面挪动的那个值。这个挪动和前面介绍的插入排序的挪动有点类似,只不过插入排序的挪动是一个个往前比较(二分法插入例外),而这个挪动是每间隔step进行比较然后确定是否挪动。我们来看下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
private static void shellSort2(int[] arr) {
    int j;
    int len = arr.length;
    for (int step = len >> 1; step > 0; step >>= 1) {
        for (int i = step; i < len; i++) {
            int temp = arr[i];
            for (j = i; j >= step && temp < arr[j - step]; j -= step) {
                arr[j] = arr[j - step];
            }
            arr[j] = temp;
        }
    }
}

在上面希尔排序中我们使用的间隔是数组长度的一半,这个间隔实际上是可以自己定的,但一定要保证间隔最后的一步是1即可,希尔排序中大家都比较认可的一种计算间隔的公式是step = step * 3 + 1;我们再来看一下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void shellSort3(int[] data) {
    int step = 1;
    while (step <= data.length / 3) {
        step = step * 3 + 1;
    }
    while (step > 0) {
        for (int i = step; i < data.length; i += step) {
            if (data[i] < data[i - step]) {
                int tmp = data[i];
                int j = i - step;
                while (j >= 0 && data[j] > tmp) {
                    data[j + step] = data[j];
                    j -= step;
                }
                data[j + step] = tmp;
            }
        }
        step = (step - 1) / 3;
    }
}

十,计数排序

计数排序是一个非基于比较的排序算法,他首先要找到数组的最大值和最小值然后再根据最大值和最小值申请频率表,其实就是个数组,每个数在数组中出现的频率。这里数组的每个值我们暂且以桶来表示,每个桶对应一个数在原数组中出现的频率,如果一个桶为1就表示和这个桶对应的这个数在原数组中只出现一次,如果为2就表示出现两次……,我们通过一张图来看下计数排序

他是首先找到数组的最大值和最小值,然后申请桶的大小,然后再一个个读取数组中的值存到桶中,最后再根据桶的顺序把桶中的数字一个个读取到数组中。每个数存放到桶中的位置是当前数字前去最小值,因为这样可以保证负数也能进行排序。我们看下的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
    public static void countSort1(int[] array) {
        int arrayLength = array.length;
        int max = array[0];
        int min = array[0];
        for (int i = 0; i < arrayLength; i++) {
            if (array[i] > max)
                max = array[i];
            else if (array[i] < min)
                min = array[i];
        }
           
        int bucketLength = max - min + 1;//桶的数量
        int[] tmp = new int[arrayLength];
        int[] buckets = new int[bucketLength];//桶
        for (int i = 0; i < arrayLength; i++) {
            buckets[array[i] - min]++;//落在某个桶里就加1
        }
        // 从小到大排序
        for (int i = 1; i < bucketLength; i++) {
            //后面桶对前面桶的累加
            buckets[i] = buckets[i] + buckets[i - 1];
        }
        // 从大到小排序
//        for (int i = bucketLength - 1; i > 0; i--) {
//            buckets[i - 1] = buckets[i] + buckets[i - 1];
//        }
        //把原数组保存在临时数组中。
        System.arraycopy(array, 0, tmp, 0, arrayLength);
        for (int k = 0; k < arrayLength; k++) {
            //根据每个数值在桶中的顺序重新存储
            array[--buckets[tmp[k] - min]] = tmp[k];
        }
    }

十一,位图排序

位图排序也称为bitmap排序,它主要用于海量数据去重和海量数据排序,假如说有10亿个int类型且全部不相同的数据,给1G内存让你排序,你怎么排,如果全部加载到内存中,相当于40亿个字节,大概约等于4G内存。所以全部加载到内存肯定不行,如果我们使用位图排序的话,我们用long类型表示,一个long占用8个字节也就是64位,所以如果我们使用位图排序的话只会占用约0.125G内存,内存占用大大减少。但位图排序有个缺点就是数据不能有重复的,如果有重复的会覆盖掉,这也是位图能在海量数据中去重的原因,我们看下位图排序的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private static int[] bitmapSort1(int[] array) {
    int max = getMaxNumbit1(array);
    int N = max / 64 + 1;
    long[] bitmap = new long[N];
    for (int i = 0; i < array.length; i++)
        bitmap[array[i] / 64] |= 1L << (array[i] % 64);
    int k = 0;
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < 64; j++) {
            if ((bitmap[i] & (1L << j)) != 0) {
                array[k++] = i * 64 + j;
            }
        }
    }
    if (k < array.length)
        return Arrays.copyOfRange(array, 0, k);
    return array;
}
     
private static int getMaxNumbit1(int array[]) {
    int max = array[0];
    for (int i = 1, length = array.length; i < length; i++) {
        if (array[i] > max) {
            max = array[i];
        }
    }
    return max;
}

我们看到这是使用的是位表示,一个long类型占8个字节,但他可以表示64个数字,所以内存占用会大大减少。最后有个k < array.length的判断,是因为如果有重复的数据会覆盖掉重复的,导致数组变小。但这里面还有个问题就是不能有负数出现,如果出现负数会报异常,我们也可以改一下让负数也可以排序,看代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private static int[] bitmapSort2(int[] array) {
    int[] value = getMaxNumbit2(array);
    int N = (value[0] - value[1]) / 64 + 1;
    long[] bitmap = new long[N];
    for (int i = 0; i < array.length; i++)
        bitmap[(array[i] - value[1]) / 64] |= 1L << ((array[i] - value[1]) % 64);
    int k = 0;
    int[] temp = new int[array.length];
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < 64; j++) {
            if ((bitmap[i] & (1L << j)) != 0) {
                temp[k++] = i * 64 + j + value[1];
            }
        }
    }
    if (k < temp.length)
        return Arrays.copyOfRange(temp, 0, k);
    return temp;
}
    
private static int[] getMaxNumbit2(int array[]) {
    int max = array[0];
    int min = array[0];
    for (int i = 1, length = array.length; i < length; i++) {
        if (array[i] > max) {
            max = array[i];
        } else if (array[i] < min) {
            min = array[i];
        }
    }
    return new int[]{max, min};
}

我们来找几行数据测试一下

1
2
3
4
5
6
7
8
int[] array2 = new int[20];
Random random = new Random();
for (int i = 0; i < array2.length; i++) {
    array2[i] = random.nextInt(1000) - 500;
}
System.out.println("排序前:" + Arrays.toString(array2));
int temp2[] = bitmapSort2(array2);
System.out.println("排序后:" + Arrays.toString(temp2));

再来看一下运行的结果

1
2
排序前[-212, -71, -58, -427, 132, -223, -315, 153, 72, -270, 15, -268, 284, 93, 234, 488, -407, -168, 277, 174]
排序后[-427, -407, -315, -270, -268, -223, -212, -168, -71, -58, 15, 72, 93, 132, 153, 174, 234, 277, 284, 488]

十二,Bogo排序

Bogo排序完全是一种搞笑的排序,到现在为止我还真没有在什么地方发现他使用过。他排序的原理是这样的,就是随机打乱数组,然后再验证数组是否有序,如果无序再打乱再验证,直到验证完全有序为止。我们看下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static final Random random = new Random();
          
private static void bogoSort(int[] array) {
    while (!isOrder(array)) {
        for (int i = 0; i < array.length; i++) {
            int randomPosition = random.nextInt(array.length);
            int temp = array[i];
            array[i] = array[randomPosition];
            array[randomPosition] = temp;
        }
    }
}
          
private static boolean isOrder(int[] array) {
    for (int i = 0; i < array.length - 1; i++) {
        if (array[i] > array[i + 1]) return false;
    }
    return true;
}

十三,鸡尾酒排序

鸡尾酒排序它是冒泡排序的一种,他和冒泡排序的不同之处在于,冒泡排序是往一个方向排的,而鸡尾酒排序是往两个方向排的,它是先从左往右把大的排到右边,然后再从右往左把小的排到左边,我们看下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void cocktailSort1(int[] array) {
    for (int i = 0; i < array.length / 2; i++) {
        //把大的挪到右边
        for (int j = i; j < array.length - i - 1; j++) {
            if (array[j] > array[j + 1]) {
                swap1(array, j, j + 1);
            }
        }
        //把小的挪到前面
        for (int j = array.length - 1 - (i + 1); j > i; j--) {
            if (array[j] < array[j - 1]) {
                swap1(array, j, j - 1);
            }
        }
    }
}
         
public static void swap1(int[] A, int i, int j) {
    A[i] = A[i] + A[j];
    A[j] = A[i] - A[j];
    A[i] = A[i] - A[j];
}

每次循环的时候都是把剩余最大的挪到右边,剩余最小的挪到左边。我们也可以换一种写法,分别用两个指针表示,一个指向前面一个指向后面,然后两个指针分别都往中间移,指针走过的地方都是已经排序好的,只要左边指针小于右边指针就一直走下去,我们看下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void cocktailSort2(int[] array) {
    int left = 0, right = array.length - 1;
    while (left < right) {
        for (int i = left; i < right; i++)
            if (array[i] > array[i + 1]) {
                swap2(array, i, i + 1);
            }
        right--;
        for (int i = right; i > left; i--)
            if (array[i - 1] > array[i]) {
                swap2(array, i, i - 1);
            }
        left++;
    }
}
        
public static void swap2(int[] A, int i, int j) {
    A[i] = A[i] + A[j];
    A[j] = A[i] - A[j];
    A[i] = A[i] - A[j];
}

十四,鸽巢排序

鸽巢排序(Pigeonhole sort),也被称作基数分类,是一种不可避免遍历每一个元素并且排序的情况下效率最好的一种排序算法。但它只有在差值(或者可被映射在差值)很小的范围内的数值排序的情况下实用,如果差值相差很大的话很浪费空间。他和计数排序很像,原理基本上都差不多,应该算是计数排序的一个变种吧。他们都是先申请一个数组,想当于桶,然后遍历待排数组,并标记待排数据在桶中相对应位置的数量。然后遍历桶。不同的是计数排序最后先把原始数据存放到一个临时数组中,然后遍历临时数组,并根据临时数组在桶中的位置,再重新存放到原始数组中。而鸽巢排序最后是直接遍历桶,如果有值就把他写入到原始数组中。如果待排数据数量小但相差很大的话,鸽巢排序最后要执行的次数很多,而计数排序最后要执行的次数比较少。我们看下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static void pigeonholeSort1(int[] array) {
    int max = max1(array);
    int bucket[] = new int[max + 1];
    for (int i = 0; i < array.length; ++i)
        bucket[array[i]]++;
    int j = 0;
    for (int i = 0; i < bucket.length; ++i)
        for (int k = 0; k < bucket[i]; ++k)
            array[j++] = i;
}
       
private static int max1(int[] array) {
    int max = array[0];
    for (int i = 0; i < array.length; i++) {
        if (array[i] > max)
            max = array[i];
    }
    return max;
}

这里会有个问题,就是只能对非负数进行排序,如果出现负数则会出现异常,我们在改一下,让他正数负数都可以排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static void pigeonholeSort2(int[] array) {
    int maxCount[] = getMaxNumbit2(array);
    int bucket[] = new int[maxCount[0] - maxCount[1] + 1];
    for (int i = 0; i < array.length; ++i)
        bucket[array[i] - maxCount[1]]++;
    int j = 0;
    for (int i = 0; i < bucket.length; ++i)
        for (int k = 0; k < bucket[i]; ++k)
            array[j++] = i + maxCount[1];
}
      
private static int[] getMaxNumbit2(int array[]) {
    int max = array[0];
    int min = array[0];
    for (int i = 1, length = array.length; i < length; i++) {
        if (array[i] > max) {
            max = array[i];
        } else if (array[i] < min) {
            min = array[i];
        }
    }
    return new int[]{max, min};
}

OK,这就是上面介绍的14中排序算法。