java -- 排序算法
查找和排序算法是算法的入门知识,其经典思想可以用于很多算法当中。因为其实现代码较短,应用较常见。所以在面试中经常会问到排序算法及其相关的问题。但万变不离其宗,只要熟悉了思想,灵活运用也不是难事。一般在面试中最常考的是快速排序和归并排序,并且经常有面试官要求现场写出这两种排序的代码。对这两种排序的代码一定要信手拈来才行。还有插入排序、冒泡排序、堆排序、基数排序、桶排序等。
各种排序算法的比较:
插入排序
插入排序就像在分牌时可能要整理自己的牌,牌多的时候怎么整理呢?就是拿到一张牌,找到一个合适的位置插入。这个原理其实和插入排序是一样的。设计思想:每次插入之前,检查前面数据,如果小于当前数据,就往后移动一位,到无法移动时,就插入数据。
举个栗子:
对5,3,8,6,4这个无序序列进行简单插入排序,首先假设第一个数的位置时正确的,想一下在拿到第一张牌的时候,没必要整理。然后3要插到5前面,把5后移一位,变成3,5,8,6,4.想一下整理牌的时候应该也是这样吧。然后8不用动,6插在8前面,8后移一位,4插在5前面,从5开始都向后移一位。注意在插入一个数的时候要保证这个数前面的数已经有序。
代码如下:
public static void insertSort(int[] arr) {
if(arr == null || arr.length == 0)
return ;
for(int i=1; i//假设第一个数位置时正确的;要往后移,必须要假设第一个。
int j = i;
int target = arr[i]; //待插入的
//前面比target大的数都往后移
while(j > 0 & target < arr[j-1]) {
arr[j] = arr[j-1];
j --;
}
//插入
arr[j] = target;
}
}
希尔排序
希尔排序是插入排序的一种高效率的实现,基本思想是:因为数据基本有序的情况下,插入排序是非常快的。根据步长进行两个数据的比较,如果小于就对调;走完一遍后,计算步长,在进行前面的比较;余此类推,当步长为1时,数据已经比较有序了,最后一次进行插入排序。
举个栗子:
初始数组: [26, 53, 67, 48, 57, 13, 48, 32, 60, 50 ]
结果数组: [13, 26, 32, 48, 48, 50, 53, 57, 60, 67]
代码如下:
public static void shellSort(int data[]) {
if (data == null) return;
int j = 0;
int temp = 0;
int len = data.length / 2; //计算步长
for (int increment = len; increment > 0; increment /= 2) {
for (int i = increment; i < data.length; i++) {
temp = data[i];
for (j = i; j >= increment; j -= increment) {
if(temp < data[j - increment]){
data[j] = data[j - increment];
}else{
break;
}
}
data[j] = temp;
}
}
}
选择排序
选择排序的思想其实和冒泡排序有点类似,都是在一次排序后把最小的元素放到最前面。但是过程不同,冒泡排序是通过相邻的比较和交换。而选择排序是通过对整体的选择。
举个栗子:
对5,3,8,6,4这个无序序列进行简单选择排序,首先要选择5以外的最小数来和5交换,也就是选择3和5交换,一次排序后就变成了3,5,8,6,4,第二次就找出比5小的数找到4,找到4与5交换后变成3,4,8,6,5,对剩下的序列一次进行选择和交换,最终就会得到一个有序序列。
优点:
其实选择排序可以看成冒泡排序的优化,因为其目的相同,只是选择排序只有在确定了最小数的前提下才进行交换,大大减少了交换的次数。
代码如下:
public static void selectSort(int[] a) {
if (a == null) return;
int min = 0;
int i = 0;
int j = 0;
int index = 0;
int len = a.length;
for (i = 0; i < len - 1; i++) {
min = a[i];
index = i;
for (j = i + 1; j < len; j++) {
if (a[j] < min) {
min = a[j];
index = j;
}
}
a[index] = a[i];
a[i] = min;
}
}
堆排序
堆排序是借助堆来实现的选择排序,思想同简单的选择排序。
堆排序要解决的问题
如何由一个无序序列键成一个堆?
如何在输出堆顶元素之后,调整剩余元素成为一个新的堆?
问题一
可以直接使用线性数组来表示一个堆,由初始的无序序列建成一个堆就需要自底向上从第一个非叶元素开始挨个调整成一个堆。
问题二
首先是将堆顶元素和最后一个元素交换。然后比较当前堆顶元素的左右孩子节点,因为除了当前的堆顶元素,左右孩子堆均满足条件,这时需要选择当前堆顶元素与左右孩子节点的较大者(大顶堆)交换,直至叶子节点。我们称这个自堆顶自叶子的调整成为筛选。
代码如下:
public static void heapSort(int[] array) {
if (array == null) return;
buildHeap(array);//构建堆
int n = array.length;
int i = 0;
for (i = n - 1; i >= 1; i--) {
swap(array, 0, i);
heapify(array, 0, i);
}
}
//构建
public static void buildHeap(int[] array) {
int n = array.length;//数组中元素的个数
for (int i = n / 2 - 1; i >= 0; i--)
heapify(array, i, n);
}
public static void heapify(int[] A, int idx, int max) {
int left = 2 * idx + 1;// 左孩子的下标(如果存在的话)
int right = 2 * idx + 2;// 左孩子的下标(如果存在的话)
int largest = 0;//寻找3个节点中最大值节点的下标
if (left < max && A[left] > A[idx])
largest = left;
else
largest = idx;
if (right < max && A[right] > A[largest])
largest = right;
if (largest != idx) {
swap(A, largest, idx);
heapify(A, largest, max);
}
}
public static void swap(int[] array, int i, int j) {
int temp = 0;
temp = array[i];
array[i] = array[j];
array[j] = temp;
}
冒泡排序
冒泡排序是最简单的排序之一了,其大体思想就是通过与相邻元素的比较和交换来把小的数交换到最前面。这个过程类似于水泡向上升一样,因此而得名。
举个栗子:
对5,3,8,6,4这个无序序列进行冒泡排序。首先从后向前冒泡,4和6比较,把4交换到前面,序列变成5,3,8,4,6。同理4和8交换,变成5,3,4,8,6,3和4无需交换。5和3交换,变成3,5,4,8,6,3.这样一次冒泡就完了,把最小的数3排到最前面了。对剩下的序列依次冒泡就会得到一个有序序列。
public static void bubbleSort(int[] arr) {
if(arr == null || arr.length == 0)
return ;
for(int i=0; i) {
for(int j=arr.length-1; j>i; j--) {
if(arr[j]<arr[j-1]) {
swap(arr, j-1, j);
}
}
}
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
快速排序
快速排序是冒泡排序的改进版,由于排序效率在同为O(N*logN)的几种排序方法中效率较高,因此经常被采用。特点是,挖坑填数 + 分治法。
基本思路
1.先从数列中取出一个数作为基准数。
2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
3.再对左右区间重复第二步,直到各区间只有一个数。
对挖坑填数进行总结:
1.i =L; j = R; 将基准数挖出形成第一个坑a[i]。
2.j--由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中。
3.i++由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中。
4.再重复执行2,3二步,直到i==j,将基准数填入a[i]中。
代码如下:
//快速排序 ,l是0,r是数组的length
void quickSort(int s[], int l, int r) {
if (l < r) {
//Swap(s[l], s[(l + r) / 2]); //将中间的这个数和第一个数交换 这是其他快速排序可能会用
// 初始化
int i = l, j = r, x = s[l];
while (i < j) {
// 从右向左找第一个小于x的数,如果没找到,就一直往左查
while(i < j && s[j] >= x)
j--;
// 当找到,就把元素挖出来,填左边的坑,同时右边留一个坑
if(i < j) {
s[i] = s[j];
// 左边指针向右移一位,后面就要用这个指针对应的数进行比较
i++;
}
// 从左向右找第一个大于等于x的数,如果没找到,就一直向右查
while(i < j && s[i] < x)
i++;
// 当找到,就把元素挖出来,填左边的坑,同时右边留一个坑
if(i < j) {
s[j] = s[i];
// 右边指针向左移一位,后面就要用这个指针对应的数进行比较
j--;
}
}
// 用初始值X填上最后剩下的坑
s[i] = x;
quick_sort(s, l, i - 1); // 递归调用
quick_sort(s, i + 1, r);
}
}
归并排序
归并排序是另一种不同的排序方法,因为归并排序使用了递归分治的思想,所以理解起来比较容易。其基本思想是,先递归划分子问题,然后合并结果。把待排序列看成由两个有序的子序列,然后合并两个子序列,然后把子序列看成由两个有序序列。。。。。倒着来看,其实就是先两两合并,然后四四合并。。。最终形成有序序列。
代码如下:
// low是0,high是数组的length
public static int[] mergeSort(int[] nums, int low, int high) {
if (nums == null || low < 0 || low > nums.length-1 || high < 0) return nums;
int mid = (low + high) / 2;
if (low < high) {
// 左边
mergeSort(nums, low, mid);
// 右边
mergeSort(nums, mid + 1, high);
// 左右归并
merge(nums, low, mid, high);
}
return nums;
}
public static void merge(int[] nums, int low, int mid, int high) {
int[] temp = new int[high - low + 1];
int i = low;// 左指针
int j = mid + 1;// 右指针
int k = 0;
// 把较小的数先移到新数组中
while (i <= mid && j <= high) {
if (nums[i] < nums[j]) {
temp[k++] = nums[i++];
} else {
temp[k++] = nums[j++];
}
}
// 把左边剩余的数移入数组
while (i <= mid) {
temp[k++] = nums[i++];
}
// 把右边边剩余的数移入数组
while (j <= high) {
temp[k++] = nums[j++];
}
// 把新数组中的数覆盖nums数组
for (int k2 = 0; k2 < temp.length; k2++) {
nums[k2 + low] = temp[k2];
}
}
总结
- 在平均情况下,快速排序最快;
- 在最好情况下,插入排序和起泡排序最快;
- 在最坏情况下,堆排序和归并排序最快。