今天整理几种比较喜欢的排序算法:
原理:每一个元素都和它后面的所有元素比较,如果有必要,则交换,以此类推,直到倒数第二个元素和最后一个元素比较并交换完毕。
这是最简单的排序法,虽然他的时间复杂度不是最小的,甚至可以说是比较大的。但是由于代码结构十分简单,所以实现起来十分容易,对于小规模数据的排序来说,bubble sort是很好的。
/** * bubble sort * best: O(n) * worst: O(n^2) * average: O(n^2) */
public static void bubbleSort() {
int tmp;
for (int i = 0; i < a.length - 1; i++) {
for (int j = i + 1; j < a.length; j++) {
if (a[i] > a[j]) {
tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
}
}
}
原理:首先将整个数组看成两部分,前面是已经排序完成的数组,后面是还未排序完成的。每次从未排序的数组当中选择一个(其实就是第一个啦~~方便遍历嘛),对排序好的数组进行遍历对比,找到合适的位置并插入。
插入法排序其实和冒泡法从代码上看十分相似。区别在于冒泡法是两次同向遍历,而插入法是一前一后异向遍历。
此法需要一点逆向思维,虽然不如冒泡法好记忆。但也是十分简单的一种排序法。
/** * insertion Sort * best: O(n) * worst: O(n^2) * average: O(n^2) */
public static void insertSort() {
int tmp;
for (int i = 1; i < a.length; i++) {
for (int k = i; k > 0; k--) {
if (a[k] < a[k - 1]) {
tmp = a[k];
a[k] = a[k - 1];
a[k - 1] = tmp;
}
}
}
}
原理:选择排序就更简单了,首先从全部数组中选出最小的,放在第一位,再从剩下的数组当中选出最小的,放在第二位,依此类推直到所有的数都选择完毕。
选择排序可以说是最容易理解的自然方法(就是一般人在日常生活中使用的方法)了,但是在程序当中却可以说是最慢的,因为无论何种情况下,它的时间复杂度都是O(n^2)。
/** * Selection Sort * best: O(n^2) * worst: O(n^2) * average: O(n^2) */
public static void selectionSort() {
for (int i = 0; i < a.length; i++) {
int min = a[i];//假设当前元素就是最小数
int p = i;//记录当前元素的坐标
for (int j = i; j < a.length; j++) {//向后遍历,找到最小数,并记录坐标
if (a[j] < min) {
min = a[j];
p = j;
}
}
//将找到的最小数与当前元素交换
int tmp = a[p];
a[p] = a[i];
a[i] = tmp;
}
}
原理:归并排序,将整个数列两分,然后再将子数组两份,然后再两分,一直到不可分为止,然后再将这些子数组比较组合,直到得到完整的数组。
归并排序,就是所谓的Divide and Conquer,利用递归的优势,将整个数组”分而治之”,最后再整合起来。
这个方法应该是比较快的(当然还有更快的所谓bucket sort,不过呵呵。。。那种方法局限性太大了),就是对编程的要求略高。
/** * Merge Sort * best: O(n*logn) * worst: O(n*logn) * average: O(n*logn) */
public static void mergeSort() {
mergeSort(0, a.length - 1);
}
private static void mergeSort(int first, int last) {
if (first < last) {
int mid = (first + last) / 2;
mergeSort(first, mid);//递归左边数组
mergeSort(mid + 1, last);//递归右边数组
merge(first, mid, last);//组合当前的数组
}
}
private static void merge(int first, int mid, int last) {
int[] tmp = new int[last - first + 1];
int ll = first;
int rl = mid + 1;
int k = 0;
//两个都不为空
while (ll <= mid && rl <= last) {
tmp[k++] = (a[ll] < a[rl]) ? a[ll++] : a[rl++];
}
//左边不空
while (ll <= mid) {
tmp[k++] = a[ll++];
}
//右边不空
while (rl <= last) {
tmp[k++] = a[rl++];
}
for (int i = 0; i < k; i++)
a[first + i] = tmp[i];
}
原理:先选定一个中心数pivot,随便选,可以是第一个,也可以是最后一个,甚至可以是中间随便一个,然后遍历整个数组,将大于pivot的数放在右边,小于pivot的数放在左边,然后再分别递归左右两边的子数组。
这个算法也是采用分治的思想(Divid and Conquer),基本上采用分治思想的算法,时间复杂度都和n*logn有关,所以效率都还行~~~~
这个算法其实好理解,难点应该在代码的实现上,如果你没有记住代码的话,重新编写可能会遇到两个问题
public static void quickSort() {
quickSort(0, a.length - 1);
}
private static synchronized void quickSort(int first, int last) {
if (first < last) {
int pivot = partition(first, last);
//这里要注意,递归的时候需要绕过已经选出来了的中心数pivot
quickSort(first, pivot - 1);
quickSort(pivot + 1, last);
}
}
private static int partition(int first, int last) {
int s = first;
int tmp;
for (int l = first; l < last; l++) {
if (a[l] < a[last]) {
tmp = a[l];
a[l] = a[s];
a[s] = tmp;
s++;
}
}
tmp = a[last];
a[last] = a[s];
a[s] = tmp;
return s;
}
原理:
heap sort放在最后面讲,其实是因为它最难以理解,无论从代码实现还是理论知识方面都是。但是它的效率又相当优秀,所以不得不学习。。。
学习heap sort(堆排序)之前,首先要理解binary tree(二叉树)的概念,然后再理解binary heap(二叉堆)。如果懒得看wiki也没有关系,简单来说,所谓的二叉堆就是下面这个样子。
上图是一个最大二叉堆,它有三个特性,我归纳如下:
理解二叉堆的概念以后,我们就可以知道,如果一个数组的排列符合二叉堆的结构,我们就可以轻易的得到它的最大值或是最小值,所谓排序,就是不断取出这些最值的过程
所以说堆排序的主要目的,其实就是把目标数组弄成二叉堆。
说起来简单,但是做起来却不容易,第一个难点,就是一般人很难将上图的二叉堆,和代码里面的数组联系起来,也就是说无法理解,这么一棵二维的树,怎么能放进一维的数组里面呢?
先说方法:
如图所示,根结点放在0位,左儿子放在1位,右儿子放在2位,以此类推,左儿子的左儿子放在3位,左儿子的右儿子放在4位。
直观点儿来讲,就是把二叉堆,从上到下,从左到右依次放入数组就行了。。。
但是这还是按照”人“的思路来操作的,那么如何在代码中实现呢?或者说,数学规律是什么呢?
总的来说,就是对于第 n 个结点,它的左儿子,放在 2n+1 位,右儿子则放在 2n+2 了。
反过来讲,对于一个数组来说,无论它里面的元素如何排列,只要我们按照上面的规律来访问它,那么它就可以看成是一个二叉树
我至今还对想出这个方法的前辈表示无尽的膜拜 Orz。思路怎么来的,我就不讲了,数学证明什么的,你也不要想了。。。因为我也说不清楚,我只能拾人牙慧,直接讲解决方法了。
找到了这个规律,我们就可以把一个数组当成是binary tree(二叉树)来操作,并把它弄成binary heap(二叉堆)。
如何在代码中实现二叉树到二叉堆的转换呢?
有两种方式:从上往下,或者从下往上,本文只介绍从下往上的方式。
以最大二叉堆为例
通过二叉堆的特性我们可以看出,如果把它放入数组当中,那么最后一个元素必定是叶子,而这个叶子的根,也应该是整个二叉堆当中的最后一个拥有叶子的结点,在它之前的所有结点,都应该拥有叶子!
如果我们从这个结点开始,遍历之前所有的结点,那么我们实际上就对所有的元素进行了操作。
根据数学规律,假设最后一个元素的坐标是 n=2i+1 或 n=2i+2 ,那么最后一个拥有叶子的结点的坐标就是 i=n/2−1 ,因为无论 n 是基数还是偶数,对于对于代码来说都一样,因为 n/2 是向下取整的。
知道了从哪里开始,还要知道怎么做。
我们需要做的就是保证以该结点为根,它下面所有的结点所组成的二叉树,是二叉堆。
我们先找出结点所对应的两片叶子当中较大的那片,再与结点比较,如果结点比该叶子大,则表明这是二叉堆,退出操作,如果结点比该叶子小,则交换结点与叶子,交换以后,因为叶子变成了较小的数,所以它可能比它的叶子还要小。所以如果这片叶子向下还有叶子,则应该将这片叶子看成结点,循环执行上面的操作,如果这片叶子下面没有叶子,则程退出循环。
moveDown流程图:
moveDown函数代码:
private static void moveDown(int i, int n) {
int lc = 2 * i + 1;
while (lc < n) {
if (lc < n - 1 && a[lc] < a[lc + 1]) {
lc++;
}
if (a[i] < a[lc]) {
int tmp = a[i];
a[i] = a[lc];
a[lc] = tmp;
i = lc;
lc = 2 * i + 1;
}
else {
lc = n;
}
}
}
这里需要说明的是,如果是从上往下的做法,这个函数并不适用,因为它无法完全遍历整个二叉树,故而它没办法保证整个二叉树就是二叉堆,它只能保证,有交换动作的那一个分支符合二叉堆的特性,但由于我们是从下往上操作,在所有下层的数据率先符合二叉堆的前提下,这种不完全的遍历反而大大的提高了执行效率
主函数分成两步:
public static void heapSort() {
for (int i = a.length / 2 - 1; i >= 0; i--) {
moveDown(i, a.length);
}
for (int i = a.length - 1; i >= 1; i--) {
int tmp = a[i];
a[i] = a[0];
a[0] = tmp;
moveDown(0, i);
}
}
最后贴出完整的代码如下:
public static void heapSort() {
for (int i = a.length / 2 - 1; i >= 0; i--) {
moveDown(i, a.length);
}
for (int i = a.length - 1; i >= 1; i--) {
int tmp = a[i];
a[i] = a[0];
a[0] = tmp;
moveDown(0, i);
}
}
private static void moveDown(int i, int n) {
int lc = 2 * i + 1;
while (lc < n) {
if (lc < n - 1 && a[lc] < a[lc + 1]) {
lc++;
}
if (a[i] < a[lc]) {
int tmp = a[i];
a[i] = a[lc];
a[lc] = tmp;
i = lc;
lc = 2 * i + 1;
}
else {
lc = n;
}
}
}
计算步骤放这里方便以后复习用~~
以merge sort为例:
假设数组的长度是 2k ,分治法会将这个数组一分为二,每段数组长度就是 2k−1 。
然后再将这两个数组组合起来,以上面的程序为例,组合的函数就是
private static void merge(int first, int mid, int last) {
int[] tmp = new int[last - first + 1];
int ll = first;
int rl = mid + 1;
int k = 0;
//两个都不为空
while (ll <= mid && rl <= last) {
tmp[k++] = (a[ll] < a[rl]) ? a[ll++] : a[rl++];
}
//左边不空
while (ll <= mid) {
tmp[k++] = a[ll++];
}
//右边不空
while (rl <= last) {
tmp[k++] = a[rl++];
}
for (int i = 0; i < k; i++)
a[first + i] = tmp[i];
}
中,赋值有4步,比较有n次,最后把临时数组放入原数组又用了n次。那么一共有 2n+4 步。
这样一来,得出递推公式:
n=2k,i<k,i∈N
T(n)=2T(n2)+2n+4=2T(n2)+4∗(n2+1)=...
⇒T(n)=2iT(n2i)+i∗2n+8∗(2i−1)
当 i=k 时
⇒T(n)=2kT(1)+k∗2n+8∗(2k−1)
因为 T(1) 时,数组不需要操作,故 T(1)=0
⇒T(n)=k∗2n+8∗(2k−1)
又因为 2k=n⇒k=logn
⇒T(n)=2n∗logn+8∗(n−1)
⇒O(n)=n∗logn
所以时间复杂度就是 O(n∗logn) !!!!!