一、排序算法概述
排序,即将一组数据按照递增或递减的规则进行排列。根据不同的规则,排序算法的分类也不尽相同,常见分类标准有:内部排序和外部排序。内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。常见的内部排序算法有八种:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。外部排序的话,采用多路归并排序算法,将文件划分为几个能读入内存的小部分,然后分别读入进行排序,经过多次处理即可完成对大文件的排序。
排序算法也是一种基本的算法,使用时要考虑两大因素有:算法效率和排序速度。每一种排序算法都有着各自的特点,有时候选择合适的算法达到执行效率至关重要,因此,不同排序算法的出现体现了对高效排序的追求。
二、冒泡排序
冒泡排序,是一种最简单、最直观的排序方式,属于交换排序范畴,核心思路是相邻的数据进行交换达到排序目的。
冒泡排序的步骤:
1)比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2)对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
3)针对所有的元素重复以上的步骤,除了最后一个。
4)持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
冒泡排序Demo演示:
/** 冒泡排序 */
public class ArraySortDemo{
public static void main(String[] args) {
int [] num = {5,3,7,99,23,6,20,18,32,45};
for (int i = 0; i < num.length-1; i++) {
for (int j = 0; j < num.length-1-i; j++) {
if(num[j]>num[j+1]){
// 使用临时变量交换元素
int temp = num[j];
num[j] = num[j+1];
num[j+1] = temp;
}
}
}
}
}
三、快速排序
快速排序,也属于交换排序范畴,可以看作是冒泡排序的改进版,核心思路是分治思想和递归思想,即将数据按照基准值划分为两部分,小于基准值的放在一边,大于基准值的放在另一边,然后再选择一个基准值多次递归上述操作,最终达到交换排序目的。
快速排序的步骤:
1)选定一个分界值作为基准,将数组一分为二。
2)数组的所有元素比基准值小的会放在基准的左边,所有元素比基准值大的会在基准的右边。
3)基准的左边和基准的右边又可以各选定一个分界值作为基准,继续排序,递归的进行上述过程,最终整个数组就会按从小到大顺序完成排序。
快速排序Demo演示:
/** 快速排序 */
public class ArraySortDemo{
public static void main(String[] args) {
int [] num = {5,3,7,99,23,6,20,18,32,45};
fast(num, 0, num.length-1);
}
/** 对数组 或数组指定范围的元素进行排序 */
public static void fast(int[] arry,int first,int last) {
if(last > first) {
// 选择基准值
int pivotIndex = fastvoid(arry,first,last);
// 前半部分
fast(arry,first,pivotIndex-1);
// 后半部分
fast(arry,pivotIndex+1,last);
}
}
public static int fastvoid(int[] arry,int first,int last) {
int privot = arry[first];
int low = first + 1;
int high = last;
while(low < high) {
// 从前向后和从后向前依次和主元比较,当前面的元素比主元大,后面的元素比主元小则这两个元素互换位置
while(low <= high && arry[low] <= privot) {
low++;
}
while(low <= high && arry[high] >= privot) {
high--;
}
if(high > low) {
int temp = arry[high];
arry[high] = arry[low];
arry[low] = temp;
}
}
while(high > first && arry[high] >= privot) {
high--;
}
if(privot > arry[high]) {
arry[first] = arry[high];
arry[high] = privot;
return high;
}else {
return first;
}
}
}
四、选择排序
选择排序,是一种比较直观的排序方式,核心思路是将未排序的最大(小)的元素放在已排序元素的后面,重复该步骤直至完成排序。
选择排序的步骤:
1)首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
2)再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
3)重复第二步,直到所有元素均排序完毕。
选择排序Demo演示:
/** 选择排序 */
public class ArraySortDemo{
public static void main(String[] args) {
int [] num = {5,3,7,99,23,6,20,18,32,45};
for (int i = 0; i < num.length; i++) {
for (int j = i+1; j < num.length; j++) {
if(num[i]>num[j]){
// 不使用临时变量交换元素
num[i] = num[i] + num[j];
num[j] = num[i] - num[j];
num[i] = num[i] - num[j];
}
}
}
}
}
五、堆排序
堆排序,是利用堆结构特点所设计的一种排序算法,核心思路是利用二叉树的一些性质完成排序的。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
堆排序的步骤:
1)创建一个堆H[0..n-1],把堆首(最大值)和堆尾互换。
2)把堆的尺寸缩小1,并调用shift_down(0),目的是把新的数组顶端数据调整到相应位置。
3) 重复上述步骤,直到堆的尺寸为1。
堆排序Demo演示:
/** 堆排序 */
public class ArraySortDemo{
public static void main(String[] args) {
int [] num = {5,3,7,99,23,6,20,18,32,45};
headSort();
}
public static void headSort(int[] list) {
// 构造初始堆,从第一个非叶子节点开始调整,左右孩子节点中较大的交换到父节点中
for (int i = (list.length) / 2 - 1; i >= 0; i--) {
headAdjust(list, list.length, i);
}
// 排序,将最大的节点放在堆尾,然后从根节点重新调整
for (int i = list.length - 1; i >= 1; i--) {
int temp = list[0];
list[0] = list[i];
list[i] = temp;
headAdjust(list, i, 0);
}
}
private static void headAdjust(int[] list, int len, int i) {
int k = i, temp = list[i], index = 2 * k + 1;
while (index < len) {
if (index + 1 < len) {
if (list[index] < list[index + 1]) {
index = index + 1;
}
}
if (list[index] > temp) {
list[k] = list[index];
k = index;
index = 2 * k + 1;
} else {
break;
}
}
list[k] = temp;
}
}
六、插入排序
插入排序,是一种最简单直观的排序算法,核心思路是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
插入排序的步骤:
1)将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
2)从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。
插入排序Demo演示:
/** 插入排序 */
public class ArraySortDemo{
public static void main(String[] args) {
for(int i=1;i<num.length;i++){
int temp = num[i];
// 如果索引为i比i-1小,则把i插入到i-1处
if(temp<num[i-1]){
int j = i-1 ;
// 插入i到i-1时,要把i-1之后的(包含i-1)所以元素都向后移动一遍
for(;j>=0&&num[j]>temp;j--){
num[j+1] = num[j];
}
// 最后把i放在合适的位置
num[j+1] = temp;
}
}
}
}
七、希尔排序
希尔排序,是一种递减增量的排序算法,可以看作是插入排序的改进版。核心思路是先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
希尔排序的步骤:
1)选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
2)按增量序列个数k,对序列进行k 趟排序;
3)每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
希尔排序Demo演示:
/** 希尔排序 */
public class ArraySortDemo{
public static void main(String[] args) {
int [] num= {5,3,7,99,23,6,20,18,32,45};
int gap = 1;
while (gap < num.length / 3) {
gap = gap * 3 + 1;
}
for (; gap > 0; gap /= 3) {
int i, j, temp;
for (i = gap; i < num.length; i++) {
temp = num[i];
for (j = i - gap; j >= 0 && num[j] > temp; j -= gap) {
num[j + gap] = num[j];
}
num[j + gap] = temp;
}
}
}
}
八、归并排序
归并排序,也称为合并排序,是建立在归并操作上的一种有效的排序算法,核心思路是分治思想和递归思想,将两个或两个以上的有序表组合成一个新的有序表。
归并排序的步骤:
1)申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列。
2)设定两个指针,最初位置分别为两个已经排序序列的起始位置。
3)比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置。
4)重复步骤3直到某一指针达到序列尾。
5)将另一序列剩下的所有元素直接复制到合并序列尾。
归并排序Demo演示:
/** 归并排序 */
public class ArraySortDemo{
public static void mergeSort(int[] data,int left,int right){
if (left>= right) return;
// 找出中间索引
int center= (left + right) / 2;
// 对左边数组进行递归
mergeSort(data,left, center);
// 对右边数组进行递归
mergeSort(data,center + 1, right);
// 合并
merge(data,left, center, right);
}
public static void merge(int[] data, int left, int center, int right) {
int[]tmpArr = new int[data.length];
// 右数组第一个元素索引
int mid =center + 1;
// third 记录临时数组的索引
int third =left;
// 缓存左数组第一个元素的索引
int tmp =left;
while (left<= center && mid <= right) {
// 从两个数组中取出最小的放入临时数组
if(data[left] <= data[mid]) {
tmpArr[third++] = data[left++];
} else{
tmpArr[third++] = data[mid++];
}
}
// 剩余部分依次放入临时数组(实际上两个while只会执行其中一个)
while (mid<= right) {
tmpArr[third++] = data[mid++];
}
while (left<= center) {
tmpArr[third++] = data[left++];
}
// 将临时数组中的内容拷贝回原数组中
// (原left-right范围的内容被复制回原数组)
while (tmp<= right) {
data[tmp] = tmpArr[tmp++];
}
}
}
九、基数排序
基数排序,是一种非比较型整数排序算法,核心思路是将整数按位数切割成不同的数字,然后按每个位数分别比较。将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零,然后从最低位开始,依次进行一次排序,这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
简单来说,就是把数据分组,放在一个个的桶中,然后对每个桶里面的在进行排序。比如对大小为[1..1000]范围内的n个整数A[1..n]排序,首先,可以把桶设为大小为10的范围,具体而言,设集合B[1]存储[1..10]的整数,集合B[2]存储 (10..20]的整数,……集合B[i]存储( (i-1)*10, i*10]的整数,i = 1,2,..100。总共有 100个桶。然后,对A[1..n]从头到尾扫描一遍,把每个A[i]放入对应的桶B[j]中。 再对这100个桶中每个桶里的数字排序,这时可用冒泡,选择,乃至快排,一般来说任 何排序法都可以。最后,依次输出每个桶里面的数字,且每个桶中的数字从小到大输出,这 样就得到所有数字排好序的一个序列了。
值得注意的是,前面说的几大排序算法 ,大部分时间复杂度都是O(n2),也有部分排序算法时间复杂度是O(nlogn)。而桶式排序却能实现O(n)的时间复杂度。但桶排序的缺点是:空间复杂度比较高,需要的额外开销大。排序有两个数组的空间开销,一个存放待排序数组,一个就是所谓的桶,比如待排序值是从0到m-1,那就需要m个桶,这个桶数组就要至少m个空间;其次待排序的元素都要在一定的范围内等等。
基数排序Demo演示:
/** 基数排序 */
public class RadixSort {
public void radixSort(int[] array) {
int max = getMax(array);
int bit = 1;
while(max / bit > 0) {
radix(array, bit);
bit *= 10;
}
}
private void radix(int[] array, int bit) {
int[] temp = new int[array.length];
int[] bucket = new int[10];
for(int i = 0; i < array.length; i++) {
bucket[(array[i] / bit) % 10]++;
}
for(int i = 1; i < bucket.length; i++) {
bucket[i] += bucket[i-1];
}
for(int i = array.length - 1; i >= 0; i--) {
temp[bucket[(array[i] / bit) % 10] - 1] = array[i];
bucket[(array[i] / bit) % 10]--;
}
for(int i = 0; i < temp.length; i++) {
array[i] = temp[i];
}
}
private int getMax(int[] array) {
int max = array[0];
for(int i = 1; i < array.length; i++){
if(array[i] > max) {
max = array[i];
}
}
return max;
}
public static void main(String[] args) {
int[] array = {0,2,11,3,1,5,9,8,7};
new RadixSort().radixSort(array);
for (int i : array) {
System.out.println(i);
}
}
}
十、排序算法的特性总结
常用排序算法的时间复杂度、空间复杂度及稳定性,如图所示: