最近在找工作时很多面试官都会问到排序算法的实现,所以趁着周末有时间就来总结一下七种排序算法实现。
算法的实现我使用的是java语言,其中为了增强算法的可复用性,我使用了泛型这一特性,在排序的数组元素都要实现Comparable接口,在排序时使用Comparable接口中的方法compareTo来对两个元素进行比较。
一、冒泡排序。
在当初大一时候学习C语言,当时教的两种排序算法冒泡排序、简单选择排序印象都很深刻,应该是我学过的排序算法中最简单的了。
冒泡排序的思想:
1.第一次排序时将序列[0 ~ n - 1]中从前往后进行两个相邻元素的比较,若前者较大则交换,比较n-1次;
当第一趟排序结束时,序列最大的元素就被交换到位置n-1上,就如同一个气泡,一步一步往后翻滚,直到最后一位。
2.重复步骤1,在第i趟时需要翻滚n-i-1次,每趟决定一个元素的位置,一共需要n-1趟。
比如,初始序列: [1, 5, 4, 3, 2]
第1趟: [1, 4, 3, 2 ] 5
第2趟: [1, 3, 2 ] 4, 5
......
public static > void bubbleSort(T[] a) {
for (int i = 0; i < a.length - 1; i++) {
for (int j = 0; j < a.length - i - 1; j++) {
if (a[j].compareTo(a[j + 1]) > 0) {
T temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
}
}
其实优化的冒泡排序应该有第3步,就是在一趟排序中未交换元素则说明子序列已经有序,不在进行下一趟排序;所以冒泡排序最多执行n-1次。
比如,初始序列:[1, 2, 3, 5, 4]
只需要一趟排序:[1, 2, 3, 4 ] 5
下面这个是我实现的优化冒泡算法,因为多了赋值语句和条件判断,所以在数组前面序列有序的情况下运行时间会有优化,但是如果数组元素分布均匀,大小随机则反而效率会低下。
public static > void bubbleSortOpt(T[] a) {
for (int i = 0; i < a.length; i++) {
boolean isSwap = false;
for (int j = 0; j < a.length - i - 1; j++) {
if (a[j].compareTo(a[j + 1]) > 0) {
T temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
isSwap = true;
}
}
i = isSwap ? i : a.length;
}
}
而下面这个是在我的数据结构课本上的实现,不使用条件判断跳出循环,解决了效率问题。经过多次测试,下面的排序算法的运行时间确实比上面两个要好。
public static > void bubbleSortOpti(T[] a) {
int i = a.length - 1, j, last;
while (i > 0) {
last = 0;
for (j = 0; j < i; j++) {
if (a[j].compareTo(a[j + 1]) > 0) {
T temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
last = j;
}
}
i = last;
}
}
优化冒泡排序在已经有序的情况下只需进行一趟排序,n-1次比较。因此最好情况下的时间复杂度为O(n),无需移动元素;
最坏情况进行n-1趟排序,第i趟比较n-i次,移动元素3(n-i)次,这样总的比较次数为(1/2)n(n-1),移动元素次数为3n(n-1)/2。最坏情况下时间复杂度为O(n^2)。
二、简单选择排序。
简单选择排序的基本思想:
1.第一趟在初始序列[0 ~ n-1]中找最小值元素,并与位置为0的元素交换位置。
2.第i趟在序列[i-1, n-1]中进行,找到最小值元素与位置为i-1的元素交换位置。每次决定一个元素的位置,经过n-1趟后使得初始序列有序。
比如,初始序列:[1, 5, 4, 3, 2]
第1趟:1 [5, 4, 3, 2]
第2趟:1 2 [4, 3, 5]
......
public static > void selectSort(T[] a) {
for (int i = 0; i < a.length - 1; i++) {
int k = i;
for (int j = i + 1; j < a.length; j++) {
if (a[k].compareTo(a[j]) > 0) {
k = j;
}
}
if (k > i) {
T temp = a[i];
a[i] = a[k];
a[k] = temp;
}
}
}
上面这种方法是标记了最小元素的位置,在一趟排序的最后交换序列首元素和序列最小元素。之前我还写过一种,在写这篇博客之前我一直以为是冒泡排序,后来才发现是选择排序。我贴出来对比一下,这种写法也是每次确定最小元素的位置但是没有标记而是在一趟排序时进行多次交换元素,这样做影响了效率不推荐这样做。
public static > void selectSortE(T[] a) {
for (int i = 0; i < a.length - 1; i++) {
for (int j = i + 1; j < a.length; j++) {
if (a[i].compareTo(a[j]) > 0) {
T temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
}
}
简单选择排序和数组序列的初始排列无关,无聊排列如何都必须要执行n-1趟。总的比较次数为n(n-1)/2,交换元素(n-1)次,移动元素3(n-1)次。最好、最坏和平均时间复杂度都为O(n^2)。
三、直接插入排序
直接插入排序的基本思想:
1.向数组序列后面检索,当检索到后一个元素小于前一个元素时,记录下元素。从记录的元素位置开始往前检索,同时元素往后移找到比记录下元素要小的元素的位置插入。
2.重复1的步骤,经过n-1趟排序后即成为有序序列。
比如,初始序列:[1, 5, 4, 3, 2]
第1趟:[1, (4, 5), 3, 2]
第2趟:[1, (3, 4, 5), 2]
第3趟:[1, (2, 3, 4, 5)]
public static > void insertSort(T[] a) {
for (int i = 1; i < a.length; i++) {
int j = i;
T temp = a[i];
for (; j > 0 && temp.compareTo(a[j - 1]) < 0; j--) {
a[j] = a[j - 1];
}
a[j] = temp;
}
}
直接插叙排序在有序的情况下,总的比较次数是n-1,移动元素次数是2(n-1),最好情况下时间复杂度就是O(n)。最坏情况下,一趟最多比较i次,移动元素i+2次,总的比较次数为n(n-1)/2,移动元素次数(n+4)(n-1)/2。最坏情况时间复杂度为O(n^2)。
四、快速排序。
快速排序可以说是冒泡排序的高级版本,冒泡排序每次都只能对相邻的两个数进行比较,而快速排序从两端同时检索保证每趟排序结束,基准数左边的元素都小于基准数,基准数右边的元素都大于基准数。代码中是选中序列第一个元素作为基准,对快排来说还有很多优化的方法,下次有时间的时候再总结一下。
快速排序的思想:
1.先从序列后端往前检索,找到小于基准数的元素,再从序列前端往后检索,找到大于基准数的数交换两个元素位置。直到从后往前和从前往后的指针相遇,
将基准数的位置和相遇位置元素交换结束一趟排序。这样就划分出两个序列,一个序列的元素比基准数都要小,一个序列的元素比基准数都要大。
2.将划分出的子序列按1的步骤继续排序,直到子序列只有一个元素时得到有序序列。
比如,初始序列:[1, 5, 4, 3, 6, 2]
第1趟:[1, (5, 4, 3, 6, 2)]
第2趟:[1, (2, 4, 3), 5, (6)]
第3趟:[1, 2, (4, 3), 5, 6]
第4趟:[1, 2, 3, 4, 5, 6]
public static > void quickSort(T[] a) {
quickSort(a, 0, a.length - 1);
}
public static > void quickSort(T[] a, int left, int right) {
if (left >= right) return;
T temp = a[left];
int i = left;
int j = right;
while (i < j) {
while(a[j].compareTo(temp) >= 0 && i < j) j--;
while(a[i].compareTo(temp) <= 0 && i < j) i++;
if (i < j) {
T t = a[i];
a[i] = a[j];
a[j] = t;
}
}
a[left] = a[j];
a[j] = temp;
quickSort(a, left, j - 1);
quickSort(a, j + 1, right);
}
快排最坏情况是每次分割的两个子序列中有一个为空,机初始序列有序(顺序或逆序),这是快速排序效率最低,时间复杂度为O(n^2)。快排的平均时间复杂度为O(nlogn)。在最坏情况下快排需要的附加堆栈存储空间为O(n)。
五、堆排序。
使用数据结构堆来辅助我们排序,如果要构造非递减序列我们采用大根堆。大根堆是包含n个结点的完全二叉树,该树中每个节点的关键字值小于等于其双亲节点的关键字值。
堆排序的思想:
1.将初始序列构造成大根堆,从堆中第一个非叶子节点开始调用adjustDown方法将元素向下调整,直到堆顶。
2.第i趟排序将堆顶元素a[0]与堆底元素a[n-i]交换,然后将a[0]向下调整,直到堆中只剩最后一个元素。
比如,初始序列:[1, 5, 4, 3, 2]
建堆后:[5, 3, 4, 1, 2]
第1趟:[4, 3, 1, 2] 5
第2趟:[3, 1, 2] 4, 5
第3趟:[1, 2] 3, 4, 5
......
public static > void heapSort(T[] a) {
for (int i = (a.length - 2) / 2; i >= 0; i--) {
adjustDown(a, i, a.length - 1);
}
for (int i = a.length - 1; i >= 0; i--) {
T temp = a[0];
a[0] = a[i];
a[i] = temp;
adjustDown(a, 0, i - 1);
}
}
public static > void adjustDown(T[] a, int r, int j) {
T temp = a[r];
int child = 2 * r + 1;
while (child <= j) {
if (child < j && a[child].compareTo(a[child + 1]) < 0) child++;
if (temp.compareTo(a[child]) >= 0) break;
a[(child - 1) / 2] = a[child];
child = 2 * child + 1;
}
a[(child - 1) / 2] = temp;
}
构建堆得时间最多是O(nlogn),在每趟排序后都要向下调整一次最多花费O(nlogn),所以堆排序的时间复杂度为O(nlogn)。
六、希尔排序。
据百度百科,希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。
希尔排序的思想:
1.第1趟以数组长度的一般作为分量d,后面的步骤与直接插入排序基本一样,只是循环移动的步长为d。
2.在一趟排序之后将增量除以2,继续执行插入排序,直到d为1。
比如,初始序列:[1, 5, 4, 3, 2]
第1趟:[1, 3, 4, 5, 2] d=2
第2趟:[1, 3, 2, 5, 4]
第3趟:[1, 2, 3, 5, 4] d=1
第4趟:[1, 2, 3, 4, 5]
public static > void shellSort(T[] a) {
int d = a.length / 2;
while (d >= 1) {
for (int i = d; i < a.length; i++) {
int j = i;
T temp = a[i];
for (; j - d >= 0 && temp.compareTo(a[j - d]) < 0; j -= d) {
a[j] = a[j - d];
}
a[j] = temp;
}
d /= 2;
}
}
据百度百科,希尔排序的时间性能优于直接排序的原因:
- 当文件初态基本有序时,直接插入排序所需的比较和移动次数均较少。
- 当n值较小时,n和n2差别也小,即直接插入排序的最好时间复杂和最坏时间复杂度O(n2)差别不大。
- 在希尔排序开始时增量较大,分组较多,每组记录数目少,故组内直接插入较快后来增量减少,而组内的记录数目逐渐增多,
但由于已经按2d最为距离排过序,使文件较接近于有序状态,所以一趟排序过程也快。
希尔排序的平均时间复杂度为O(n^1.3)。
七、归并排序。
归并排序思想:
将有n个元素的序列看成是n个长度为1的有序子序列,然后两两合并子序列,得到[n/2]个长度为2或1的有序子序列;
在两两合并,知道得到一个长度为n的有序序列时结束。
比如,初始序列:[1, 5, 4, 3, 2]
第1趟:[(1, 5), 4, (2, 3)]
第2趟:[(1, 4, 5), (2, 3)]
第3趟:[1, 2, 3, 4, 5]
public static > void mergeSort(T[] a) {
mergeSort(a, 0, a.length - 1);
}
public static > void mergeSort(T[] a, int left, int right) {
if (left >= right) return;
int mid = (left + right) / 2;
mergeSort(a, left, mid);
mergeSort(a, mid + 1, right);
merge(a, left, right, mid);
}
public static > void merge(T[] a, int left, int right, int mid) {
Object[] temp = new Object[right - left + 1];
int i = left, j = mid + 1, k = 0;
while (i <= mid && j <= right) {
if (a[i].compareTo(a[j]) <= 0) {
temp[k++] = a[i++];
} else {
temp[k++] = a[j++];
}
}
while (i <= mid) temp[k++] = a[i++];
while (j <= right) temp[k++] = a[j++];
for (i = 0; i < temp.length; i++) {
a[left + i] = (T) temp[i];
}
}
两路合并算法的时间复杂度为o(nlogn),附加辅助空间为O(n)。
总结一下
|排序算法|平均时间复杂度|最好情况时间复杂度|最坏情况时间复杂度|最坏空间复杂度|稳定性|
|-|
|冒泡排序|O(n2)|O(n)|O(n2)|O(1)|稳定|
|简单选择排序|O(n2)|O(n2)|O(n^2)|O(1)|不稳定|
|直接插入排序|O(n2)|O(n)|O(n2)|O(1)|稳定|
|快速排序|O(nlogn)|O(nlogn)|O(n^2)|O(n)|不稳定|
|堆排序|O(nlogn)|O(nlogn)|O(nlogn)|O(1)|不稳定|
|希尔排序|O(n1.3)|O(n)|O(n2)|O(1)|不稳定|
|归并排序|O(nlogn)|O(nlogn)|O(nlogn)|O(n)|稳定|
最近开了一个个人博客网站,但是不懂怎么用wordpress=。=
欢迎前来提意见我的博客