最经典的、最常用的排序算法:
1. 时间复杂度O(n2),空间复杂度O(1) :冒泡排序、插入排序、选择排序
2. 时间复杂度O(nlogn) :归并排序、快速排、堆排序
3. 时间复杂度O(n) :计数排序、基数排序、桶排序
1.1 冒泡排序
它会遍历若干次要排序的数列,每次遍历时,它都会从前往后依次的比较相邻两个数的大小;如果前者比后者大,则交换它们的位置。这样,一次遍历之后,最大的元素就在数列的末尾! 采用相同的方法再次遍历时,第二大的元素就被排列在最大元素之前。重复此操作,直到整个数列都有序为止!
private int[] bubbleSort(int [] a) {
for (int i = 0; i < a.length; i++){
// 提前退出冒泡循环的标志位
boolean flag = false;
for (int j = 0; j < a.length - i -1; j++) {
if (a[j] > a[j+1]) {
a[j] ^= a[j+1];
a[j+1] ^= a[j];
a[j] ^= a[j+1];
flag = true;
}
}
//没有交换,则说明已经排好序了
if (!flag) break;
}
return a;
}
1.2 插入排序
首先,我们将数组中的数据分为两个区间,已排序区间和未排序区间。初始已排序区间只有一个元素,就是数组的第一个元素。插入算法的核心思想是每次取未排序区间中的一个元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程,直到遍历完无序数据。
插入排序也包含两种操作,一种是元素的比较,一种是元素的移动。当我们需要将一个数据 a 插入到已排序区间时,需要拿 a 与已排序区间的元素依次比较大小,找到合适的插入位置。找到插入点之后,我们还需要将插入点之后的元素顺序往后移动一位,这样才能腾出位置给元素 a 插入。
插入排序每次遍历只需要执行一个赋值语句(移动数据时), 而冒泡排序需要三个赋值语句,因此插入排序性能更好;
private int[] insertSort(int [] a) {
if (a.length <= 1) return a;
for (int i = 1; i < a.length; i++) {
int value = a[i];
int j = i-1;
//从后往前比较
for (; j >= 0; j--) {
if (a[j] > value) {
a[j+1] = a[j];//移动数据
} else {
break;
}
}
//插入数据
a[j+1] = value;
}
return a;
}
1.3 选择排序
选择排序也是先将数据分为已排序区间和未排序区间。然后每次从未排序区间中找到最小的元素,将其放到已排序区间的末尾。
private int[] selectSort(int [] a) {
if (a.length <= 1) return a;
int minIndex;
for (int i = 0; i < a.length; i++) {
minIndex = i;
for (int j = i+1; j < a.length; j++) {
if (a[j] < a[minIndex]) {
minIndex = j;
}
}
//将最小值换到有序区间的末尾
if (minIndex != i) {
a[minIndex] ^= a[i];
a[i] ^= a[minIndex];
a[minIndex] ^= a[i];
}
}
return a;
}
1.4希尔排序
希尔排序(Shell Sort)是插入排序的一种,它是针对直接插入排序算法的改进。该方法又称缩小增量排序。
希尔排序实质上是一种分组插入方法。它的基本思想是:对于n个待排序的数列,取一个小于n的整数gap(gap被称为步长)将待排序元素分成若干个组子序列,所有距离为gap的倍数的记录放在同一个组中;然后,对各组内的元素进行直接插入排序。 这一趟排序完成之后,每一个组的元素都是有序的。然后减小gap的值,并重复执行上述的分组和排序。重复这样的操作,当gap=1时,整个数列就是有序的。
//希尔排序
private int[] shellSort(int[] a) {
int m, i, j, k, gap;
int n = a.length;
if (n < 1) return a;
//循环直到gap为1
for (gap = n / 3 + 1; gap >= 1; gap = gap / 3 + 1) {
//将整个数组分成gap个组,对每个组分别进行插入排序
for (m = 0; m < gap; m++) {
//插入排序,步长为gap
for (i = m + gap; i < n; i += gap) {
k = a[i];
for (j = i - gap; j >= 0 && k < a[j]; j -= gap) {
a[j + gap] = a[j];
}
a[j + gap] = k;
}
}
if (gap == 1) {
break;
}
}
return a;
}
2.1 归并排序
归并排序使用的是分治思想,递归的方式;我们先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,用递归的方式对子序列再进行分割,合并,直到区间内只剩一个元素;每次递归结束都得到一个有序的子序列;最终整个数组就都有序了。这是因为归并排序的合并函数,在合并两个子区间时,需要借助额外的存储空间, 因此空间复杂度是 O(n)。
private int[] mergeSort(int[]a, int p, int q) {
if (p >= q) {
return a;
}
int m = (q + p) /2;
mergeSort(a, p, m);
mergeSort(a, m+1, q);
merge(a, p, m, q);
return a;
}
private int[] 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[j] > a[i]){
temp[k++] = a[i++];
} else {
temp[k++] = a[j++];
}
}
while (i <= mid) {
temp[k++] = a[i++];
}
while (j <= end) {
temp[k++] = a[j++];
}
for (int i1 = 0; i1 < temp.length; i1++) {
a[start++] = temp[i1];
}
return a;
}
2.2 快速排序
选择一个基准数,通过一趟排序将要排序的数据分割成独立的两部分;其中一部分的所有数据都比另外一部分的所有数据都要小。然后,再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
如果要排序数组中下标从 p 到 r 之间的一组数据,我们选择 p 到 r 之间的任意一个数据作为 pivot(分区点)。我们遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,将大于 pivot 的放到右边,将 pivot 放到中间。经过这一步骤之后,数组 p 到 r 之间的数据就被分成了三个部分,前面 p 到 q-1 之间都是小于 pivot 的,中间是 pivot,后面的 q+1 到 r 之间是大于 pivot 的。根据分治、递归的处理思想,我们可以用递归排序下标从 p 到 q-1 之间的数据和下标从 q+1 到 r 之间的数据,直到区间缩小为 1,就说明所有的数据都有序了。
private int[] quickSort(int[]a, int start, int end) {
if (start >= end) {
return a;
}
int q = partition(a, start, end); //获取分区点
quickSort(a, start, q-1);
quickSort(a, q+1, end);
return a;
}
private int partition(int[] a, int start, int end) {
int i = start;
int j = start;
int pivot = a[end];
while (j < end) {
if (a[j] < pivot) {
int temp = a[j];
a[j] = a[i];
a[i] = temp;
i++;
}
j++;
}
int temp = a[end];
a[end] = a[i];
a[i] = temp;
return i;
}
2.3 堆排序
堆排序(Heap Sort)是指利用堆这种数据结构所设计的一种排序算法。
因此,学习堆排序之前,有必要了解堆
我们知道,堆分为"最大堆"和"最小堆"。最大堆通常被用来进行"升序"排序,而最小堆通常被用来进行"降序"排序。
最大堆进行升序排序的基本思想:
① 建堆:将数列a[1...n]构造成最大堆。
② 排序:将a[1]和a[n]交换,使a[n]是a[1...n]中的最大值;然后将a[1...n-1]重新调整为最大堆。 接着,将a[1]和a[n-1]交换,使a[n-1]是a[1...n-1]中的最大值;然后将a[1...n-2]重新调整为最大值。 依次类推,直到整个数列都是有序的。
//堆排序
private int[] heapSort(int[] a) {
int n = a.length -1;//最后一个元素下标
//1.建堆
buildHeap(a, n);
//2.排序
while (n>0) {
//把堆顶元素和和数组最后的元素交换,数组最后一个元素就是最大值
swap(a, 0, n);
n--;
//将前n-1个元素再构造成最大堆
heapify(a, 0, n);
}
return a;
}
//数组下标从0开始,左子节点下标2*i+1,右子节点下标2*i+2
//构造大顶推
private void buildHeap(int[]a, int n) {
int i = (n-1)/2; //从最后一个非叶子节点开始(数组最后一个元素的父节点),从后往前遍历
while(i>=0) {
heapify(a, i, n);
i--;
}
}
//从上往下堆化
private void heapify(int[]a, int i, int n) {
int maxPos = i;
while(true) {
if (2*i+1 <= n && a[i] < a[2*i+1]) maxPos = 2*i+1;
if (2*i+2 <= n && a[maxPos] < a[2*i+2]) maxPos = 2*i+2;
if (maxPos == i) break; //没有子节点了
swap(a, i, maxPos);
i = maxPos;
}
}
private void swap(int[] a, int i, int j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}