目录
前言
最近在做题的过程中遇到很多排序类的题目,主要是快速排序和归并排序。因此特意总结一下十大经典排序算法,希望可以更好的理解其核心思想和特性。
首先排序算法可以分为内部排序算法和外部排序算法:在内存中进行的称为内部排序算法,也就是这里所说的这十种算法;相应的,当数据量很大时无法全部拷贝到内存需要使用外存,称为外部排序算法。接下来我们可用如下表来简单概括这十种算法:
表中数据说明:
稳定:如果A原本在B前面,而A=B,排序之后A仍然在B的前面;
不稳定:如果A原本在B的前面,而A=B,排序之后A可能会出现在B的后面;
时间复杂度: 描述一个算法执行所耗费的时间;
空间复杂度:描述一个算法执行所需内存的大小;
n:数据规模;
k:“桶”的个数;
In-place:占用常数内存,不占用额外内存;
Out-place:占用额外内存。
该十种排序算法可分为如下所示的两大类
- 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
- 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
一、冒泡排序(Bubble Sort)
算法步驟
- 比较相邻的元素,如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素作同样的比价,从开始第一对到结尾的最后一对,这样在最后的元素就是最大的数;
- 针对所有的元素重复以上的步骤,除了数组最后已经排好序的数组;
- 重复步骤1~3,直到排序完成。
代码实现
public static void bubbleSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j+1]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
二、选择排序(Selection Sort)
算法步驟
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置;
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾;
- 重复第2步,直到所有元素均排序完毕。
代码实现
public static void selectionSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
if (minIndex != i) {
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
}
三、插入排序(Insertion Sort)
算法步驟
- 首先从第一个元素开始,该元素被认为是有序的;
- 取出下一个元素,在已经排序的元素序列中从后往前进行扫描;
- 如果该已排好序的元素大于新元素,则将该元素移到下一位置;
- 重复步骤3一直往前进行扫描比较,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
代码实现
public class InsertionSort {
public static void insertionSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int val = arr[i], j = i;
while (j > 0 && val < arr[j - 1]) {
arr[j] = arr[j - 1];
j--;
}
arr[j] = val;
}
}
}
四、希尔排序(Shell Sort)
算法步驟
- 选择一个增量序列{t1, t2, …, tk};
- 按增量序列个数k,对序列进行k趟排序;
- 每趟排序,根据对应的增量t,将待排序列分割成若干长度为m的子序列,分别对各子表进行直接插入排序。仅增量因子为1时,整个序列作为一个表来处理,表长度即为整个序列的长度。
其中,增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2, (n/2)/2, …, 1},称为增量序列。一般的增量序列都选择以上说明的这个,但不一定是最优的。
代码实现
public static void shellSort(int[] arr) {
int n = arr.length;
int gap = n / 2;
while (gap > 0) {
for (int i = gap; i < n; i++) {
int j = i;
int temp = arr[i];
while (j >= gap && arr[j - gap] > temp) {
arr[j] = arr[j - gap];
j -= gap;
}
arr[j] = temp;
}
gap /= 2;
}
}
五、归并排序(Merge Sort)
算法步驟
- 如果待排序列只有一个元素,则直接返回,否则将长度为n的待排序列分成两个长度为n/2的子序列,递归进行调用进行分割知道每个子序列中只有一个元素;
- 此时的每个子序列被认为是有序的,然后递归调用的返回子序列进行两两合并;
- 合并过程中完成排序操作,具体操作为设定两个指针,分别指向两个已经排序子序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到合并返回的数组,并移动指针到下一位置;
- 重复步骤3~4直到某一指针达到序列尾;
- 将另一序列剩下的所有元素直接复制到合并序列尾,最终得到的新序列就是有序序列。
代码实现
public static void mergeSort(int[] arr) {
int n = arr.length;
if (n < 2) {
return;
}
int mid = n / 2;
int[] left = new int[mid];
int[] right = new int[n - mid];
for (int i = 0; i < mid; i++) {
left[i] = arr[i];
}
for (int i = mid; i < n; i++) {
right[i - mid] = arr[i];
}
mergeSort(left);
mergeSort(right);
merge(left, right, arr);
}
public static void merge(int[] left, int[] right, int[] arr) {
int nL = left.length;
int nR = right.length;
int i = 0, j = 0, k = 0;
while (i < nL && j < nR) {
if (left[i] <= right[j]) {
arr[k++] = left[i++];
} else {
arr[k++] = right[j++];
}
}
while (i < nL) {
arr[k++] = left[i++];
}
while (j < nR) {
arr[k++] = right[j++];
}
}
六、快速排序(Quick Sort)
算法步驟
- 从序列中随机挑出一个元素,做为基准(pivot,这里选择序列的最左边元素作为基准);
- 重新排列序列,将所有比基准值小的元素摆放在基准前面,所有比基准值大的摆在基准的后面。该操作结束之后,该基准就处于数列的中间位置。这个操作称为分区(partition);
- 递归地把小于基准值元素的子序列和大于基准值元素的子序列进行上述操作即可
代码实现
public static void quickSort(int[] arr, int left, int right) {
if (left >= right) {
return;
}
int pivotIndex = partition(arr, left, right);
quickSort(arr, left, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, right);
}
public static int partition(int[] arr, int left, int right) {
int pivotIndex = left;
int pivotValue = arr[right];
for (int i = left; i < right; i++) {
if (arr[i] < pivotValue) {
int temp = arr[i];
arr[i] = arr[pivotIndex];
arr[pivotIndex] = temp;
pivotIndex++;
}
}
int temp = arr[pivotIndex];
arr[pivotIndex] = arr[right];
arr[right] = temp;
return pivotIndex;
}
七、堆排序(Heap Sort)
算法步驟
- 将待排序列(R0, R1, ……, Rn)构建成最大堆(最小堆);
- 将堆顶元素R[0]与最后一个元素R[n]进行交换,此时得到新的无序区(R0, R1, ……, Rn-1)和新的有序区(Rn),且满足R[0, 1, ……, n-1]<=R[n](>=R[n]);
- 由于调整后的新堆可能违反堆的性质,因此需要对当前无序区(R0, R1, ……, Rn-1)进行调整;
- 重复步骤2~3直到有序区的元素个数为n。
代码实现
public class HeapSort {
private static int heapLen;
public static void heapSort(int[] arr) {
heapLen = arr.length;
for (int i = heapLen - 1; i >= 0; i--) {
heapify(arr, i);
}
for (int i = heapLen - 1; i > 0; i--) {
swap(arr, 0, heapLen - 1);
heapLen--;
heapify(arr, 0);
}
}
private static void heapify(int[] arr, int idx) {
int left = idx * 2 + 1, right = idx * 2 + 2, largest = idx;
if (left < heapLen && arr[left] > arr[largest]) {
largest = left;
}
if (right < heapLen && arr[right] > arr[largest]) {
largest = right;
}
if (largest != idx) {
swap(arr, largest, idx);
heapify(arr, largest);
}
}
private static void swap(int[] arr, int idx1, int idx2) {
int tmp = arr[idx1];
arr[idx1] = arr[idx2];
arr[idx2] = tmp;
}
}
八、计数排序(Counting Sort)
算法步驟
- 找出数组中的最大值maxVal和最小值minVal;
- 创建一个计数数组countArr,其长度是maxVal-minVal+1,元素默认值都为0;
- 遍历原数组arr中的元素arr[i],以arr[i]-minVal作为countArr数组的索引,以arr[i]的值在arr中元素出现次数作为countArr[a[i]-min]的值;
- 遍历countArr数组,只要该数组的某一下标的值不为0则循环将下标值+minVal输出返回到原数组即可。
代码实现
public static void countingSort(int[] arr) {
int n = arr.length;
if (n == 0) {
return;
}
int max = arr[0], min = arr[0];
for (int i = 1; i < n; i++) {
if (arr[i] > max) {
max = arr[i];
}
if (arr[i] < min) {
min = arr[i];
}
}
int[] count = new int[max - min + 1];
for (int i = 0; i < n; i++) {
count[arr[i] - min]++;
}
int index = 0;
for (int i = 0; i < count.length; i++) {
while (count[i] > 0) {
arr[index++] = i + min;
count[i]--;
}
}
}
九、桶排序(Bucket Sort)
算法步驟
- 设置一个bucketSize(该数值的选择对性能至关重要,性能最好时每个桶都均匀放置所有数值,反之最差),表示每个桶最多能放置多少个数值;
- 遍历输入数据,并且把数据依次放到到对应的桶里去;
- 对每个非空的桶进行排序,可以使用其它排序方法(这里递归使用桶排序);
- 从非空桶里把排好序的数据拼接起来即可。
代码实现
public static void bucketSort(int[] arr) {
int n = arr.length;
if (n == 0) {
return;
}
int max = arr[0], min = arr[0];
for (int i = 1; i < n; i++) {
if (arr[i] > max) {
max = arr[i];
}
if (arr[i] < min) {
min = arr[i];
}
}
int bucketSize = 10;
int bucketCount = (max - min) / bucketSize + 1;
List<List<Integer>> buckets = new ArrayList<>(bucketCount);
for (int i = 0; i < bucketCount; i++) {
buckets.add(new ArrayList<>());
}
for (int i = 0; i < n; i++) {
int bucketIndex = (arr[i] - min) / bucketSize;
buckets.get(bucketIndex).add(arr[i]);
}
int index = 0;
for (int i = 0; i < bucketCount; i++) {
List<Integer> bucket = buckets.get(i);
Collections.sort(bucket);
for (int j = 0; j < bucket.size(); j++) {
arr[index++] = bucket.get(j);
}
}
}
十、基数排序(Radix Sort)
算法步骤
- 取得数组中的最大数,并取得位数,即为迭代次数n(例如:数组中最大数为123,则 n=3);
- arr为原始数组,从最低位(或最高位)开始根据每位的数字组成radix数组(radix数组是个二维数组,其中一维长度为10),例如123在第一轮时存放在下标为3的radix数组中;
- 将radix数组中的数据从0下标开始依次赋值给原数组;
- 重复2~3步骤n次即可。
代码实现
import java.util.ArrayList;
import java.util.List;
//基数排序
public class RadixSort {
public static void radixSort(int[] arr) {
if (arr.length < 2) return;
int maxVal = arr[0];//求出最大值
for (int a : arr) {
if (maxVal < a) {
maxVal = a;
}
}
int n = 1;
while (maxVal / 10 != 0) {//求出最大值位数
maxVal /= 10;
n++;
}
for (int i = 0; i < n; i++) {
List<List<Integer>> radix = new ArrayList<>();
for (int j = 0; j < 10; j++) {
radix.add(new ArrayList<>());
}
int index;
for (int a : arr) {
index = (a / (int) Math.pow(10, i)) % 10;
radix.get(index).add(a);
}
index = 0;
for (List<Integer> list : radix) {
for (int a : list) {
arr[index++] = a;
}
}
}
}
}
总结
- 数据量规模较小,考虑插入或选择。当元素分布有序时插入将大大减少比较和移动记录的次数,如果不要求稳定性,可以使用选择,效率略高于插入;
- 数据量规模中等,使用希尔排序;
- 数据量规模较大,考虑堆排序(元素分布接近正序或逆序)、快速排序(元素分布随机)和归并排序(稳定性);
- 一般来说不使用冒泡。