今天开始看排序算法,渐渐 发现一个事实,脑子有点不够用……
排序算法主要的性能指标
有三个:
1.时间性能;
2.辅助空间;
3.算法复杂度
(不是时间空间复杂度,就是纯粹的代码复杂程度)。
详细概念就不抄了……反正总的来说,时间性能一般是大家最看重的,这篇文章里一共会有7种排序算法。按算法复杂度分为两类:
简单算法:冒泡排序、简单选择排序、直接插入排序;
改进算法:希尔算法、堆排序、归并排序和快速排序。
一个一个来吧~
具体排序算法
1.冒泡排序(Bubble Sort)
public class BubbleSort {
public static void main(String[] args){
int[] a = {11,25,32,1,3,4,37,12,33,13,32,10,38,58,7,4,63,33,6,43,4,21,14,24,62,4,42,1};
Tool.print(a);
int[] b = bubbleSort(a);
Tool.print(b);
}
public static int[] bubbleSort(int[] a){
int length = a.length;
for(int i=0; i<length; i++){
for(int j=0;j<length-1-i;j++){
if(a[j] > a[j+1])
Tool.swap(a, j, j+1);
}
}
return a;
}
}
其中
Tool.print(a);
…
Tool.swap(a, j, j+1);
是我自己在同一个包下编写的一个小工具类,这是链接。
代码非常简单,for双循环,内循环中每两个相邻的数都进行比较,如果前边的数比后边的数大,两个数就进行交换。外循环是控制保证每次数组中最大的数都能放到此次循环的最后面。
运行结果:
11 25 32 1 3 4 37 12 33 13 32 10 38 58 7 4 63 33 6 43 4 21 14 24 62 4 42 1
1 1 3 4 4 4 4 6 7 10 11 12 13 14 21 24 25 32 32 33 33 37 38 42 43 58 62 63
成功对数组进行了排序,其时间复杂度为O(n^2)。
2.简单选择排序(Simple Selection Sort)
public class SimpleSelSort {
public static void main(String[] args){
int[] a = {11,25,32,1,3,4,37,12,33,13,32,10,38,58,7,4,63,33,6,43,4,21,14,24,62,4,42,1};
Tool.print(a);
int[] b = simpleSelSort(a);
Tool.print(b);
}
public static int[] simpleSelSort(int[] a){
int length = a.length;
int min;
for(int i=0; i<length; i++){
min = i;
for(int j=i+1; j<length; j++){
if(a[min]>a[j]){
min = j;
}
}
if(min!=i)
Tool.swap(a, i, min);
}
return a;
}
}
代码跟冒泡排序类似,先拿出一个值a[i](一般都是数组第一个值a[0]),将它的数组下标保存到一个临时变量min,此时a[min]指向a[0]。之后开始遍历数组,如果有数比a[0]小,(例如说a[3])那就把找到的这个数a[3]的下标3赋给临时变量min,此时a[min]指向a[3],每次遇到比a[min]小的数,都将它的下标赋给min。一遍遍历结束后,我们得到了数组中最小值的下标(例如是10),之后我们将a[0]和a[10]交换,这时数组的最小值已经存到数组的第一位了。之后再拿出最小值的后面一位(a[1]),然后重复上述步骤,会得到数组中第二小的值并将其存入数组的第二位中。……最后我们会得到一个其中的数从小到大排列的数组。
11 25 32 1 3 4 37 12 33 13 32 10 38 58 7 4 63 33 6 43 4 21 14 24 62 4 42 1
1 1 3 4 4 4 4 6 7 10 11 12 13 14 21 24 25 32 32 33 33 37 38 42 43 58 62 63
排序完成,其时间复杂度和冒泡排序一样,也是O(n^2),但实际上,效率比冒泡算法稍高。3.直接插入排序(Straight Insertion Sort)
public class StraitInsertSort {
public static void main(String[] args){
int[] a = {11,25,32,1,3,4,37,12,33,13,32,10,38,58,7,4,63,33,6,43,4,21,14,24,62,4,42,1};
Tool.print(a);
int[] b = straitInsertSort(a);
Tool.print(b);
}
public static int[] straitInsertSort(int[] unSorted){
int length = unSorted.length;
for(int i=1; i<length; i++){
if(unSorted[i-1]>unSorted[i]){
int temp;
int j = i;
temp = unSorted[i];
for(; j > 0 && unSorted[j-1] > temp;j --){
unSorted[j] = unSorted[j-1]; //复制一份给后一位
}
unSorted[j] = temp; //覆盖后面数值相同两位中的第一位
}
}
return unSorted;
}
}
其代码逻辑跟上面两个排序算法稍有不同,但也差距不大。本质上都是双for循环,这也是它们时间复杂度都是O(n^2)的原因。直接插入算法是在执行内循环之前先进行一个大小判断,例如如果两个相邻的数中前一个数unSorted[3]比后一个数unSorted[4]大(我们想得到的是从小到大的排序)那就将后一个数unSorted[4]存入一个临时变量temp,之后开始进行循环判断,如果unSorted[4]前边的数有大于unSorted[4]的,那就把大的数往后移一位(此时由于unSorted[4]的值已经存入temp中,因为unSorted[3]比unSorted[4]大,那么将unSorted[3]的值直接赋给unSorted[4]的位置上,unSorted[4]的值我们仍然可以用。这样之后unSorted[3]与被覆盖后的unSorted[4](此时unSorted[4]的值是unSorted[3])都存放了unSorted[3]的值,那么对unSorted[3]又可以进行判断,修改的操作了)。直到遇见一个值,它比unSorted[4]小,那么将unSorted[4]的值存到这个数的后一位(后一位和后后一位存放的是相同的值,不怕被覆盖)。内循环完成,之后继续外循环,完成排序。11 25 32 1 3 4 37 12 33 13 32 10 38 58 7 4 63 33 6 43 4 21 14 24 62 4 42 1
1 1 3 4 4 4 4 6 7 10 11 12 13 14 21 24 25 32 32 33 33 37 38 42 43 58 62 63
其时间复杂度为O(n^2),实际中它的性能稍强于冒泡排序和简单选择排序。
4.希尔排序(Shell Sort)
public class Test1 {
public static void main(String[] args){
int[] a = getRandomArray(9546400); //生成一个长度为9546400的数组
Tool.print("--------------------------------------");
int[] b = shellSort(a);
Tool.print(b);
}
public static int[] getRandomArray(int log){
int[] result = new int[log];
for (int i = 0; i < log; i++) {
result[i] = i;
}
for (int i = 0; i < log; i++) {
int random = (int) (log * Math.random());
int temp = result[i];
result[i] = result[random];
result[random] = temp;
}
return result;
}
public static int[] shellSort(int[] a) {
int d = a.length / 2;
while (true) {
// 把距离为d的元素编为一个组,扫描所有组
for (int i = d; i < a.length; i++) {
int j = 0;
int temp = a[i];
// 对距离为d的元素组进行排序
for (j = i - d; j >= 0 && temp < a[j]; j = j - d) {
a[j + d] = a[j];
}
a[j + d] = temp;
}
if(d==1) return a;
d = d / 2; // 减小增量
}
}
}
代码的逻辑是在一个while循环里套两个for循环,虽然看着套了2个循环,但是由于每次循环都会将工作完成一部分,所以代码到最后反而效率很高。当d没有到1的时候会一直执行for循环里面的for循环,for循环中每个被分好的小组的相同位置的数会进行比较,如果前面的数比后面的大,那么进行一次交换,之后反复,直到 i超出数组的范围,跳出循环,d变成自己之前的一半,继续进行循环。最后得出正确数组。
5.堆排序(Heap Sort)
public class HeapSort {
public static void main(String[] args){
int[] a = Tool.getRandomArray(9546400); //生成一个长度为9546400的数组
Tool.print("--------------------------------------");
int[] b = heapSort(a);
Tool.printL(b);
}
public static void heapAdjust(int[] array, int parent, int length) {
int temp = array[parent]; // temp保存当前父节点
int child = 2 * parent + 1; // 先获得左孩子
while (child < length) {
if (child + 1 < length && array[child] < array[child + 1]) // 如果有右孩子结点,并且右孩子结点的值大于左孩子结点,则选取右孩子结点
child++;
if (temp >= array[child]) // 如果父结点的值已经大于孩子结点的值,则直接结束
break;
array[parent] = array[child]; // 把孩子结点的值赋给父结点
parent = child; // 选取孩子结点的左孩子结点,继续向下筛选
child = 2 * child + 1;
}
array[parent] = temp;
}
public static int[] heapSort(int[] list) {
// 循环建立初始堆
int length = list.length;
for (int i = length / 2; i >= 0; i--) {
heapAdjust(list, i, length - 1);
}
// 进行n-1次循环,完成排序
for (int i = length - 1; i > 0; i--) {
// 最后一个元素和第一元素进行交换
Tool.swap(list, 0, i);
// 筛选 R[0] 结点,得到i-1个结点的堆
heapAdjust(list, 0, i);
}
return list;
}
}
看代码可以看到,堆排序分为两个函数,其中heapSort()是最后的排序函数,而heapAjust()就是我们之前第一步里所说的先对堆进行排序,而仅靠它并不能完成使得堆中最大值处于根节点,所以我们可以再heapSort()方法中看到有一个for()循环,它从堆长度的一半开始,逐步往堆顶进行heapAjust()操作,自己稍微画草图验证一下就能知道,这样做能保证总能从堆的最底层最后一个最后一个节点所在的二叉树开始进行调整获取最大值(有点绕),然后逐步往上进行调整,这样能保证整个二叉树中的数都能被遍历,由下往上
而不会被遗漏。最终调整完后,整个数组的最大值就会在在二叉堆的根节点上(此时其它的节点都是无序而需要重新进行排序的,这点很重要)。在heapSort()的第二个for循环中交换根节点和最后一个节点(数组的最后一个值),然后继续进行调整,数组中的最大值已经被放到数组最后一位,不参加排序,其余继续上述过程,最终得到一个从小到大排好序的有序数组。6.归并排序(Merging Sort)
public class MergeSort {
public static void main(String[] args){
// int[] a = Tool.getRandomArray(9546400); //生成一个长度为9546400的数组
int[] a = {11,25,32,1,3,4,37,12,33,13,32,10,38,58,7,4,63,33,6,43,4,21,14,24,62,4,42,1};
// Tool.print(a);
Tool.print("--------------------------------------");
// int[] b = bubbleSort(a);
int[] b = mergeSort(a);
Tool.printL(b);
}
public static void merge(int[] a, int low, int mid, int high) {
int i = low; // i是第一段序列的下标
int j = mid + 1; // j是第二段序列的下标
int k = 0; // k是临时存放合并序列的下标
int[] b = new int[high - low + 1]; // array2是临时合并序列
// 扫描第一段和第二段序列,直到有一个扫描结束
while (i <= mid && j <= high) {
// 判断第一段和第二段取出的数哪个更小,将其存入合并序列,并继续向下扫描
if (a[i] <= a[j]) {
b[k++] = a[i++];
} else {
b[k++] = a[j++];
}
}
// 若第一段序列还没扫描完,将其全部复制到合并序列
while (i <= mid) {
b[k++] = a[i++];
}
// 若第二段序列还没扫描完,将其全部复制到合并序列
while (j <= high) {
b[k++] = a[j++];
}
// 将合并序列复制到原始序列中
for (k = 0, i = low; i <= high; i++, k++) {
a[i] = b[k];
}
}
public static void mergePass(int[] a, int gap, int length) {
int i = 0;
// 归并gap长度的两个相邻子表.如果i之后还有两个gap的长度,就继续循环否则跳出循环。
// 每次进行两个gap之间的归并,刚开始gap==1, 每两个数进行归并,确定大小后排好序
for (i = 0; i + 2 *gap-1 < length; i = i + 2 * gap) {
merge(a, i, i + gap - 1, i + 2 * gap - 1);
}
if (i + gap - 1 < length) {// 余下两个子表,后者长度小于gap
merge(a, i, i + gap - 1, length - 1);
}
}
public static int[] mergeSort(int[] a) {
for (int gap = 1; gap < a.length; gap = 2 * gap) {
mergePass(a, gap, a.length);
}
return a;
}
}
观察代码,可以发现代码分为三个方法merge(),mergePass(),mergeSort(),这三个方法逐渐上升层次,分别对应了不同的三个功能:7.快速排序(Quick Sort)
public class QuickSort {
public static void main(String[] args){
int[] a = Tool.getRandomArray(9546400); //生成一个长度为9546400的数组
// int[] a = {11,25,32,1,3,4,37,12,33,13,32,10,
// 38,58,7,4,63,33,6,43,4,21,14,24,62,4,42,1};
// Tool.printL(a);
Tool.print("-------------------------------------" +
"-------------------------------------");
int[] b = quickSort(a, 0, a.length-1);
Tool.printL(b);
}
public static int[] quickSort(int[] a, int left, int right){
if(left<right){
int pivot = quickAdjust(a, left, right);
quickSort(a, left, pivot);
quickSort(a, pivot+1, right);
}
return a;
}
public static int quickAdjust(int[] a, int left, int right){
int pivot = a[left];
while(left < right){
while(pivot<=a[right] && left < right)
right--;
a[left] = a[right];
while(a[left]<=pivot && left < right)
left++;
a[right] = a[left];
}
a[left] = pivot;
return left;
}
}
代码总体是比较明了的,简单说一下吧,这个快速排序中用到了递归。先找一个分界点pivot(一般直接选取数组第一个值),分界点的作用是:先将数组通过分界点分为两个较小的数组,分界点前所有的数比分界点的数都小,分界点后的数比分界点都大( 其中quickAdjust()方法实现了这一功能),再将每个较小的数组继续以相同的方法进行分离,排序……最终能得到一个排好序的数组。这样算初步完成快速排序了,但是有一点,分界点pivot选的太过随意,万一它离最值点比较近,那么快速排序的效率就会变小,所以有改进之处。一般做法是三数取中(median-of-three)法。即去三个挂念自先进行排序,再将中间数作为分界点,一般是去左端、右端和中间三个数。代码实现如下: public static int quickAdjust(int[] a, int left, int right){
// int pivot = a[left];
// 对pivot的取法进行优化
int pivot;
int mid = (left + right) /2;
if(a[left]>a[right]) //保持右端比左端大
Tool.swap(a, left, right);
if(a[mid]>a[right]) //保持右端比中间大
Tool.swap(a, mid, right);
//这时右端最大。只需要比较左端和中间,取较大值即可
if(a[mid]>a[left])
//保持左端处于中间值
Tool.swap(a, left, right);
pivot = a[left];
while(left < right){
while(pivot<=a[right] && left < right)
right--;
a[left] = a[right];
while(a[left]<=pivot && left < right)
left++;
a[right] = a[left];
}
a[left] = pivot;
return left;
}
总结
参考资料:
大话数据结构
http://www.cnblogs.com/kkun/archive/2011/11/23/2260265.html
http://www.cnblogs.com/jingmoxukong/tag/%E6%8E%92%E5%BA%8F/
算法可视化网站:
这几个网站能较为直观地显示各个算法的排序过程,能分步观察,我觉得挺适合初学者的。
https://visualgo-translation.club/zh(可能得用梯子)
http://sorting.at/