前言
这里总结一下一些常见的排序算法的原理和代码实现,主要有五种排序算法,分别如下:冒泡排序,插入排序,选择排序,归并排序和快速排序。规定排序后的数据都是从小到大。
冒泡排序(Bubble Sort)
原理: 冒泡排序只比较相邻的两个数据,如果左边的数据比右边的数据大,则交换两个数据的位置,否则不用处理。之后继续与后面相邻的数据比较,这样一次遍历之后,最大的数据就到了数组的最尾端。重复 n 次遍历(n 为数据的大小),数组中的数据就是从小到大排序好的。
看图会比较好理解:
注:图片转载自:极客时间《数据结构与算法之美》
代码实现:
// 冒泡排序,a表示数组,n表示数组大小; 最好情况时间复杂度:o(n), 最坏情况时间复杂度: o(n^2), 平均情况时间复杂度: o(n^2)。
public int[] bubbleSort(int[] a, int n) {
if (n <= 1) {
return a;
}
// i 表示遍历数组的下标。
for (int i = 0; i < n; i++) {
boolean flag = false; // 提前退出冒泡循环的标志位
for (int j = 0; j < n - i - 1; j++) { // j 表示比较的次数,
if (a[j] > a[j+1]) { // 交换数据
int temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
flag = true;
}
}
if (!flag) { //表示这一次循环没有数据交换,则表示数据已经是有序的了,循环终止。
break;
}
}
return a;
}
这里我简单说明一下: i 表示我们需要遍历的数组的下标, j 表示需要比较的次数,flag 是一个标志位,如果数据还不是有序的,则一次遍历至少需要交换一次相邻的数据,如果一次遍历完,没有需要交互的相邻数据,则说明数据已经是有序的了,则终止循环,减少代码的比较和交换次数。
时间复杂度: 冒泡排序最好情况时间复杂度:o(n), 最坏情况时间复杂度: o(n^2), 平均情况时间复杂度: o(n^2)。
插入排序(Insertion Sort)
原理: 插入排序的原理是我们把需要排序的数组分成两个区间:已排序区间和未排序区间,在最开始时,已排序区间只有一个元素,就是数组的第一个元素,之后我们遍历未排序区间的数据,依次与已排序区间的数据进行比较,将数据插入到已排序区间的某个位置中,该位置之后的数据依次往后移一位。这样直到未排序区间中的数据为空时,已排序区间中的数据就是有序的数组数据。
看图会比较好理解:
代码实现:
//插入排序, a表示数组,n表示数组大小;最好情况时间复杂度:o(n), 最坏情况时间复杂度: o(n^2), 平均情况时间复杂度: o(n^2)。
public int[] insertionSort(int[] a, int n) {
if (n <= 1) {
return a;
}
// i 表示数组的序号,从第二个数开始。
for (int i = 1; i < n; i++) {
int value = a[i]; //当前待插入的数据。
int j = i - 1; // 前一个数据序号。
for (; j >= 0; j--) {
if (a[j] > value) { // 如果前一个数据大于当前待插入数据,则将前一个数据后移,j - 1继续与待插入数据进行比较。
a[j+1] = a[j];
} else {
break; // 如果前一个数据小于等于当前待插入数据,则跳出循环。
}
}
a[j+1] = value; // 将数据插入到指定位置,由于跳出循环时 j 的值已经多减了一次 1,所以这里插入的位置要加 1。
}
return a;
}
代码注释解释的比较清楚了,我就不多赘述了。
时间复杂度:插入排序最好情况时间复杂度:o(n), 最坏情况时间复杂度: o(n^2), 平均情况时间复杂度: o(n^2)。
选择排序(Selection Sort)
原理: 选择排序的原理比较好理解。我们假设数组的第一个数据是最小的,依次遍历之后的数据,与第一个数据进行比较,如果有数据比第一个数据还小,则交换两个数据的位置。这样一次遍历下来,数组中最小的数据就已经是数组中的第一个数据了。之后重复上面的动作,数组中的数据就是有序的了。
看图会比较好理解:
代码实现:
// 选择排序,a表示数组,n表示数组大小;最好情况时间复杂度:o(n^2), 最坏情况时间复杂度: o(n^2), 平均情况时间复杂度: o(n^2)。
public int[] selectionSort(int[] a, int n) {
if (n <= 1) {
return a;
}
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (a[j] < a[i]) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
}
return a;
}
选择排序的代码实现比较简单,但是时间复杂度比较高。
时间复杂度: 最好情况时间复杂度:o(n^2), 最坏情况时间复杂度: o(n^2), 平均情况时间复杂度: o(n^2)。
总结完了三种比较简单的排序算法之后,接下来的两个排序就比较复杂了:归并排序和快速排序。实际工程中用的比较多的也是这两种排序算法。
归并排序(Merge Sort)
原理: 归并排序的原理是把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。
看图看图:
代码实现:
//归并排序
public static int[] mergeSort(int[] a, int start, int end){
int mid = (start + end) / 2;
if(start < end){
mergeSort(a, start, mid);
mergeSort(a,mid + 1, end);
//左右归并
merge(a, start, mid, end);
}
return a;
}
public static void merge(int[] a, int start, int mid, int end) {
int[] temp = new int[end-start+1];
int i= start;
int j = mid+1;
int k=0;
// 把较小的数先移到新数组中
while(i<=mid && j<=end){
if(a[i]<=a[j]){
temp[k++] = a[i++];
}else{
temp[k++] = a[j++];
}
}
// 把左边剩余的数移入数组
while(i<=mid){
temp[k++] = a[i++];
}
// 把右边边剩余的数移入数组
while(j<=end){
temp[k++] = a[j++];
}
// 把新数组中的数覆盖原来的数组a。
for(int x=0;x<temp.length;x++){
a[x+start] = temp[x];
}
}
时间复杂度: 归并排序的时间复杂度比较稳定,不管是最好情况、最坏情况,还是平均情况,时间复杂度都是 O(nlogn)。但是归并排序有个致命弱点,我们在合并数组的时候需要额外的数组存储空间,这样导致空间复杂度为 O(n)。
快速排序(Quick Sort)
原理:快排的原理是我们首先从数组中选取一个分区点(pivot),分区点可以任意,通常为数组的最后一个元素,然后遍历数组,将小于分区点的数据放到左边,大于分区点的数据放到右边,分区点在中间。根据分治、递归的处理思想,我们可以用递归排序下标从 p 到 q-1 之间的数据和下标从 q+1 到 r 之间的数据,直到区间缩小为 1,就说明所有的数据都有序了。
看图看图:
代码实现:
// 快速排序,a是数组,n表示数组的大小
public static void quickSort(int[] a, int n) {
quickSortInternally(a, 0, n-1);
}
// 快速排序递归函数,p,r为下标
private static void quickSortInternally(int[] a, int p, int r) {
if (p >= r) {
return;
}
int q = partition(a, p ,r); // 获取分区点
quickSortInternally(a, p, q-1);
quickSortInternally(a, q+1, r);
}
private static int partition(int[] a, int p, int r) {
int pivot = a[r];
int i = p;
for (int j = p; j < r; j++) {
if (a[j] < pivot) {
if (i == j) {
++i;
} else {
int tmp = a[i];
a[i++] = a[j];
a[j] = tmp;
}
}
}
int tmp = a[i];
a[i] = a[r];
a[r] = tmp;
System.out.println("i=" + i);
return i;
}
说明:快排的代码实现有点难理解,我也没理解太多,这里我先把代码实现给贴在这,方便后续查看。
时间复杂度:大部分情况下快排的时间复杂度都是O(nlogn),极端情况下为 O(n2)。不过退化到 O(n2) 的概率非常小,我们可以通过合理地选择 pivot 来避免这种情况。
最后说明:本篇文章只是笔记性质,网上有比这讲的好的多的文章,我贴出来只是方便以后查看各算法的代码实现以及复习。