八种排序算法原理及Java实现
概述
排序算法分为内部排序和外部排序,内部排序把数据记录放在内存中进行排序,而外部排序因排序的数据量大,内存不能一次容纳全部的排序记录,所以在排序过程中需要访问外存。
经常提及的八大排序算法指的就是内部排序的八种算法,分别是冒泡排序、快速排序、直接插入排序、希尔排序、简单选择排序、堆排序、归并排序和基数排序,如果按原理划分,冒泡排序和快速排序都属于交换排序,直接插入排序和希尔排序属于插入排序,而简单选择排序和堆排序属于选择排序,如上图所示。
冒泡排序
冒泡排序(Bubble Sort)是一种简单的排序算法。它重复访问要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。访问数列的工作是重复地进行直到没有再需要交换的数据,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端,像水中的气泡从水底浮到水面。
算法描述
冒泡排序算法的算法过程如下:
①. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
②. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
③. 针对所有的元素重复以上的步骤,除了最后一个。
④. 持续每次对越来越少的元素重复上面的步骤①~③,直到没有任何一对数字需要比较。
import java.util.Arrays;
/**
* 冒泡排序
* Created by zhoujunfu on 2018/8/2.
*/
public class BubbleSort {
public static void sort(int[] array) {
if (array == null || array.length == 0) {
return;
}
int length = array.length;
//外层:需要length-1次循环比较
for (int i = 0; i < length - 1; i++) {
//内层:每次循环需要两两比较的次数,每次比较后,都会将当前最大的数放到最后位置,所以每次比较次数递减一次
for (int j = 0; j < length - 1 - i; j++) {
if (array[j] > array[j+1]) {
//交换数组array的j和j+1位置的数据
swap(array, j, j+1);
}
}
}
}
/**
* 交换数组array的i和j位置的数据
* @param array 数组
* @param i 下标i
* @param j 下标j
*/
public static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
交换数字的三种方法
我们从冒泡排序的代码中看到了交换两个数字的方法 swap(int[] array, int i, int j),这里使用了临时变量,而交换数字主要有三种方法,临时变量法、算术法、位运算法、面试中经常会问到,这里简单说一下,代码如下:
import java.util.Arrays;
/**
* Created by zhoujunfu on 2018/9/10.
*/
public class SwapDemo {
public static void main(String[] args) {
// 临时变量法
int[] array = new int[]{10, 20};
System.out.println(Arrays.toString(array));
swapByTemp(array, 0, 1);
System.out.println(Arrays.toString(array));
// 算术法
array = new int[]{10, 20};
swapByArithmetic(array, 0, 1);
System.out.println(Arrays.toString(array));
// 位运算法
array = new int[]{10, 20};
swapByBitOperation(array, 0, 1);
System.out.println(Arrays.toString(array));
}
/**
* 通过临时变量交换数组array的i和j位置的数据
* @param array 数组
* @param i 下标i
* @param j 下标j
*/
public static void swapByTemp(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
/**
* 通过算术法交换数组array的i和j位置的数据(有可能溢出)
* @param array 数组
* @param i 下标i
* @param j 下标j
*/
public static void swapByArithmetic(int[] array, int i, int j) {
array[i] = array[i] + array[j];
array[j] = array[i] - array[j];
array[i] = array[i] - array[j];
}
/**
* 通过位运算法交换数组array的i和j位置的数据
* @param array 数组
* @param i 下标i
* @param j 下标j
*/
public static void swapByBitOperation(int[] array, int i, int j) {
array[i] = array[i]^array[j];
array[j] = array[i]^array[j]; //array[i]^array[j]^array[j]=array[i]
array[i] = array[i]^array[j]; //array[i]^array[j]^array[i]=array[j]
}
}
快速排序
快速排序(Quicksort)是对冒泡排序的一种改进,借用了分治的思想,由C. A. R. Hoare在1962年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
算法描述
快速排序使用分治策略来把一个序列(list)分为两个子序列(sub-lists)。步骤为:
1、首先设定一个分界值(一般都是取中间或者第一个数),通过该分界值将数组分成左右两部分;
2、将数组中大于等于分界值的数值放在分界值的右边,将数组中小于等于分界值的数值放在分界值的左边;
3、然后左右两边的数组又可以按照这个方式进行独立排序;
4、重复这个过程,可以看出这是一种递归的思想,当递归到最后,整个数组也就排序完成;
/**
* 排序算法之快速排序
* 参数arr为需要排序的数组
* 参数left为数组的起始下角标即0
* 参数right为数组的最后下角标即arr.length-1
*/
private void quickSort(int[] arr,int left,int right)
{
int f,t;
int rtemp,ltemp;
ltemp = left;
rtemp = right;
f = arr[(left+right)/2];
//经过一轮排序,已经将数组分为左右两部分
while(ltemp<rtemp)
{
while(arr[ltemp]<f)
{
++ltemp;
}
while(arr[rtemp]>f)
{
--rtemp;
}
if(ltemp<=rtemp)
{
t = arr[ltemp];
arr[ltemp] = arr[rtemp];
arr[rtemp] = t;
--rtemp;
++ltemp;
}
}
if(ltemp == rtemp)
{
ltemp++;
}
//进行递归排序
if(left<rtemp)
{
quickSort(arr,left,ltemp-1);
}
if(ltemp<right)
{
quickSort(arr,rtemp+1,right);
}
}
直接插入排序
直接插入排序的基本思想是:将数组中的所有元素依次跟前面已经排好的元素相比较,如果选择的元素比已排序的元素小,则交换,直到全部元素都比较过为止。
代码实现
提供两种写法,一种是移位法,一种是交换法。移位法是完全按照以上算法描述实,再插入过程中将有序序列中比待插入数字大的数据向后移动,由于移动时会覆盖待插入数据,所以需要额外的临时变量保存待插入数据,代码实现如下:
- 移位法:
public static void sort(int[] a) {
if (a == null || a.length == 0) {
return;
}
for (int i = 1; i < a.length; i++) {
int j = i - 1;
int temp = a[i]; // 先取出待插入数据保存,因为向后移位过程中会把覆盖掉待插入数
while (j >= 0 && a[j] > a[i]) { // 如果待是比待插入数据大,就后移
a[j+1] = a[j];
j--;
}
a[j+1] = temp; // 找到比待插入数据小的位置,将待插入数据插入
}
}
而交换法不需求额外的保存待插入数据,通过不停的向前交换带插入数据,类似冒泡法,直到找到比它小的值,也就是待插入数据找到了自己的位置。
- 交换法
public static void sort2(int[] arr) {
if (arr == null || arr.length == 0) {
return;
}
for (int i = 1; i < arr.length; i ++) {
int j = i - 1;
while (j >= 0 && arr[j] > arr[i]) {
arr[j + 1] = arr[j] + arr[j+1]; //只要大就交换操作
arr[j] = arr[j + 1] - arr[j];
arr[j + 1] = arr[j + 1] - arr[j];
System.out.println("Sorting: " + Arrays.toString(arr));
}
}
}
希尔排序
希尔排序,也称递减增量排序算法,1959年Shell发明。是插入排序的一种高速而稳定的改进版本。
希尔排序是先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
基本思想
将待排序数组按照步长gap进行分组,然后将每组的元素利用直接插入排序的方法进行排序;每次再将gap折半减小,循环上述操作;当gap=1时,利用直接插入,完成排序。
可以看到步长的选择是希尔排序的重要部分。只要最终步长为1任何步长序列都可以工作。一般来说最简单的步长取值是初次取数组长度的一半为增量,之后每次再减半,直到增量为1。更好的步长序列取值可以参考维基百科。
public class ShellSort {
public static void sort(int[] arr) {
int gap = arr.length / 2;
for (;gap > 0; gap = gap/2) {
for (int j = 0; (j + gap) < arr.length; j++) { //不断缩小gap,直到1为止
for (int k = 0; (k + gap) < arr.length; k+=gap) { //使用当前gap进行组内插入排序
if (arr[k] > arr[k+gap]) { //交换操作
arr[k] = arr[k] + arr[k+gap];
arr[k+gap] = arr[k] - arr[k+gap];
arr[k] = arr[k] - arr[k+gap];
System.out.println(" Sorting: " + Arrays.toString(arr));
}
}
}
}
}
}
选择排序
基本思想
在未排序序列中找到最小(大)元素,存放到未排序序列的起始位置。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。
public class SelectSort {
public static void sort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
int min = i;
for (int j = i+1; j < arr.length; j ++) { //选出之后待排序中值最小的位置
if (arr[j] < arr[min]) {
min = j;
}
}
if (min != i) {
arr[min] = arr[i] + arr[min];
arr[i] = arr[min] - arr[i];
arr[min] = arr[min] - arr[i];
}
}
}
不稳定排序算法,选择排序的简单和直观名副其实,这也造就了它出了名的慢性子,无论是哪种情况,哪怕原数组已排序完成,它也将花费将近n²/2次遍历来确认一遍。 唯一值得高兴的是,它并不耗费额外的内存空间。
归并排序
归并排序是建立在归并操作上的一种有效的排序算法,1945年由约翰·冯·诺伊曼首次提出。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。
基本思想
归并排序算法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
采用递归法:
①. 将序列每相邻两个数字进行归并操作,形成 floor(n/2)个序列,排序后每个序列包含两个元素;
②. 将上述序列再次归并,形成 floor(n/4)个序列,每个序列包含四个元素;
③. 重复步骤②,直到所有元素排序完毕
import java.util.Arrays;
/**
* Created by zhoujunfu on 2018/8/10.
*/
public class MergeSort {
public static int[] sort(int [] a) {
if (a.length <= 1) {
return a;
}
int num = a.length >> 1;
int[] left = Arrays.copyOfRange(a, 0, num);
int[] right = Arrays.copyOfRange(a, num, a.length);
return mergeTwoArray(sort(left), sort(right));
}
public static int[] mergeTwoArray(int[] a, int[] b) {
int i = 0, j = 0, k = 0;
int[] result = new int[a.length + b.length]; // 申请额外空间保存归并之后数据
while (i < a.length && j < b.length) { //选取两个序列中的较小值放入新数组
if (a[i] <= b[j]) {
result[k++] = a[i++];
} else {
result[k++] = b[j++];
}
}
while (i < a.length) { //序列a中多余的元素移入新数组
result[k++] = a[i++];
}
while (j < b.length) {//序列b中多余的元素移入新数组
result[k++] = b[j++];
}
return result;
}
public static void main(String[] args) {
int[] b = {3, 1, 5, 4};
System.out.println(Arrays.toString(sort(b)));
}
}