算法篇:8大排序算法总结(Java)
前言: 排序算法,排序是将一组数据依指定的顺序进行排列的过程。本文将从排序的分类,排序的原理和实现,排序的时间复杂度,排序的空间复杂度,稳定性等方面进行总结对比。(本文想要将数据完成从小到大的排序)
排序算法分类:
1)内部排序:
指将需要处理的所有数据都加载到**内部存储器(内存)中进行排序。
2)外部排序法:
数据量过大,无法全部加载到内存中,需要借助外部存储(文件等)**进行排序。
3)常见排序算法分类
内部排序(使用内存):
插入排序(直接插入排序、希尔排序)、选择排序(简单选择排序、堆排序)、交换排序(冒泡排序、快速排序)、归并排序、基数排序。其中堆排序需要用到树的知识,这次先不总结,等我学完了再来。
外部排序:使用内存外存结合
算法实现
插入排序
直接插入排序
直接插入排序就像打扑克一样,确定左边第一张牌,然后看后面的一张牌,根据大小,不断地与前面的牌的大小进行判断看是否需要插到前面去,如此下去将整副牌完成排序。
public class InsertSort {
public int[] arr;
public InsertSort(int[] arr){
this.arr = arr;
}
public int[] insertSort(){
int len = arr.length;
int index;//用来记录每次判断的元素的前一个元素的位置
int val;//用来判断每次判断元素的值
for(int i = 1; i < len; i++){
index = i - 1;//从0开始 每一次循环都会回归到判断的前方去
val = arr[i];//拿1位置的数开始和前面的判断
while (index >= 0 && arr[index] > val){
arr[index + 1] = arr[index];//将前方的值后移 以至于可以将最初i位置上的值找到符合顺序的位置
index--;//将下标不断递减 用于循环
}
//插入角标i的元素
if (index + 1 != i){
arr[index + 1] = val;
}
}
return arr;
}
}
复杂度分析
空间上看,它只需要一个记录的辅助空间,因此关键是看它的时间复杂度。空间复杂度,是O(n^2)是比较显然的。读于稳定性,观察代码,在条件判断时可以选择严格小于号,从而该算法是稳定的。
希尔排序
希尔排序是简单选择排序的一种改进。先看看简单插入排序存在的问题;数组 arr = { 2,3,4,5,6,1 } 这时候需要插入的数1最小,后移的次数明显增多,对效率有很大影响。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1 时,整个文件恰被分成一组,算法便终止。
对于分组用到的增量在学界还没有定论,一般就是从数组长度的一般开始,不断折半,直到最后增量为1,完成排序。
public class ShellSort {
public int[] arr;
public ShellSort(int[] arr) {
this.arr = arr;
}
public int[] shellSort(){
int len = arr.length;
//当分组间隔大于0时,一直循环
for (int gap = len/2; gap > 0; gap = gap/2){
for (int i = gap; i < len; i++){//这个循环意思是从数组的一半往后不断判断
//不管数组长度是奇数还是偶数都无所谓,因为分组后是有一个链接的,
// 在代码中就体现在 判断j-gap是否大于0
int j = i;//这里用j代替i是为了防止改变i的值影响上一层for循环
while(j - gap >= 0 && arr[j] < arr[j-gap] ){
//交换j 和 j - gap两点位置
int temp = arr[j-gap];
arr[j-gap] = arr[j];
arr[j] = temp;
//将j变小 和前面的比 这样才能把整个串起来 其实就是插入排序的思想了
j = j - gap;
}
}
}
return arr;
}
}
复杂度分析
希尔排序的空间复杂度是O(1),时间复杂度是一个难题 一般跟选取的组数有关,记为O(n^1.5)即可。
希尔排序是不稳定的,交换会破坏数组的稳定性。
选择排序
简单选择排序
简单选择排序的原理比较简单,从第一个位置开始考虑,选择数组中最小的数放在第一个位置;再在剩下的元素中选择最小的元素放在第二个位置,如此下去完成排序。
public class SelectSort {
public int[] arr;
public SelectSort(int[] arr){
this.arr = arr;
}
public int[] selectSort(){
int len = arr.length;
int min;//用来记录每一轮的最小值
int index;//用来记录角标,到每一轮结束后用于寻址得到每一轮的最小值
boolean flag = false;//定义一个小旗子 避免不必要的交换,
// 小旗子为false意味着在一轮比较中,原来的位置就是最小值
for(int i = 0; i < len-1; i++){
min = arr[i];
index = i;
for(int j = i + 1; j < len; j++){
if (arr[j] < min){
flag = true;
min = arr[j];//要将角标记录下来才好啊
index = j;
}
}
//一轮循环结束 可以交换了
if (flag){
arr[index] = arr[i];//index位置是每轮原来最小的位置
arr[i] = min;
flag = false;//回到原来的状态进行下一轮循环
}
}
return arr;
}
}
复杂度分析
很显然,空间复杂度是O(1),时间复杂度是O(n^2)
选择排序中也涉及到交换,从而是不稳定的。比如[5,7,5,2]
堆排序
堆排序需要用到二叉树,晚点再写。
交换排序
冒泡排序
冒泡排序的原理较为简单,就是从数组的起始位置开始,每次比较两个位置的大小,每一轮冒一个数到最顶端,这个数就是每一轮的最大数。
/**
* 冒泡排序:
* 如何优化一下:如果在一次比较后发现没有交换 那么就不用再接着排序了 用flag来表示
*/
public class BubbleSort {
public int[] arr;
public BubbleSort(int[] arr) {
this.arr = arr;
}
public int[] bubbleSort() {
int len = arr.length;
int temp;
boolean flag = true;
for (int i = 0; i < len-1; i++) {
for (int j = 0; j < len - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
flag = false;
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
while (flag){
break;
}
}
// System.out.println(flag);
return arr;
}
}
复杂度分析
空间复杂度是O(1) ,时间复杂度是O(n^2) ,冒泡排序是稳定的。
快速排序
快速排序(Quicksort)是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
public class QuickSort {
//arr表示要排序的数组 l表示左边开始的角标 r表示右边要开始的角标 都是记录从arr哪里开始算起
public static int[] quickSort(int[] arr, int left, int right){
int l = left;//左下标 数组第一个元素的角标
int r = right;//右下标 数组最后一个元素的角标
//pivot 中轴值
int pivot = arr[(left+right)/2];
int temp = 0;//临时变量,作为交换使用
//while循环的目的是让比pivot值小放到左边
//比pivot值大放到右边
while(l < r){
//在pivot的左边一直找,找到大于等于pivot值,才退出
while (arr[l] < pivot){
l += 1;
}
//在pivot的右边一直找,找到小于等于pivot值才退出
while(arr[r] > pivot){
r -= 1;
}
//这样我们其实就找到了两个角标 l是左边部分比pivot处大的第一个地方
//r是右边比pivot小的最后一个地方
// 第一遍判断之后如果l>=r说明直接就是有序的 就可以中止了
//如果l>=r说明pivot的左右两边的值,已经按照左边全部是
//小于等于pivot值,右边全部是大于等于pivot排列了
if (l >= r){
break;
}
//交换
temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
//下面这段代码到底是什么意思???????
// 这个意思是如果有一边的值和轴中值相等 那么左边固定住 右边指针减小 使得整个区间减小
// //如果交换完后,发现这个arr[l]==pivot值相等 r--,前移
if (arr[l] == pivot){
r -= 1;
}
//如果交换完后,发现这个arr[r]==pivot值相等 l++, 后移
if (arr[r] == pivot){
l += 1;
}
}
// 如果l==r,必须l++,r--,否则会出现栈溢出
if (l == r){//这是为了让l,r作为下一次递归的边界
l += 1;
r -= 1;
}
//向左递归
if (left < r){
quickSort(arr, left, r);
}
//向右递归
if (right > l){
quickSort(arr, l, right);
}
return arr;
}
复杂度分析
归并排序
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
public class MergeSort {
public static void mergeSort(int[] arr, int left, int right, int[] temp){
// right是指数组的最后一个位置数的角标
if (left < right){
int mid = (left + right)/2;//中间索引
mergeSort(arr, left, mid, temp);
mergeSort(arr, (mid + 1), right, temp);
merge(arr, left, mid, right, temp);
}
}
/**
*
* @param arr 排序的原始数组
* @param left 左边有序序列的初始索引
* @param mid 中间索引
* @param right 右边索引
* @param temp 做中转的数组
*/
public static void merge(int[] arr, int left, int mid, int right, int[] temp) {
int i = left;//初始化i,左边有序序列的初始索引
int j = mid + 1;//初始化j,右边有序序列的初始索引
int t = left;//指向temp数组的当前索引
//(一)
//先把左右两边(有序)的数据按照规则填充到temp数组
//直到左右两边的有序序列,有一边处理完毕为止
while (i <= mid && j <= right) {//继续
//如果左边的有序序列的当前元素,小于等于右边有序序列的当前元素
//即将左边的当前元素,填充到temp数组
//然后t++,i++
if (arr[i] <= arr[j]) {
temp[t] = arr[i];
t++;
i++;
} else {
temp[t] = arr[j];
t++;
j++;
}
}
//
// (二)
// 把剩余数据的一边的数据一次全部填充到temp
while (i <= mid){//左边的有序序列还有剩余的元素,就全部填充到temp了
temp[t] = arr[i];
t++;
i++;
}
while (j <= right){
temp[t] = arr[j];
t++;
j++;
}
// System.out.println("起始位置为"+left);
// System.out.println("这时候t为"+(t-1));
// System.out.println("*************************");
// (三)
// 将temp数组的元素拷贝到arr
// 注意,并不是每次都拷贝所有
int tempLeft = left;
while(tempLeft <= right){
arr[tempLeft] = temp[tempLeft];
tempLeft++;
}
}
}
复杂度分析
可以说合并排序是比较复杂的排序,特别是对于不了解分治法基本思想的同学来说可能难以理解。总时间=分解时间+解决问题时间+合并时间。分解时间就是把一个待排序序列分解成两序列,时间为一常数,时间复杂度o(1).解决问题时间是两个递归式,把一个规模为n的问题分成两个规模分别为n/2的子问题,时间为2T(n/2).合并时间复杂度为o(n)。总时间T(n)=2T(n/2)+o(n).这个递归式可以用递归树来解,其解是o(nlogn).此外在最坏、最佳、平均情况下归并排序时间复杂度均为o(nlogn).从合并过程中可以看出合并排序稳定。
用递归树的方法解递归式T(n)=2T(n/2)+o(n):假设解决最后的子问题用时为常数c,则对于n个待排序记录来说整个问题的规模为cn。
从这个递归树可以看出,第一层时间代价为cn,第二层时间代价为cn/2+cn/2=cn……每一层代价都是cn,总共有logn+1层。所以总的时间代价为cn*(logn+1).时间复杂度是o(nlogn).
空间复杂度:由于归并排序在归并过程中需要与原始记录序列同样数量的存储空间存放归并结果以及递归时深度为logn的栈空间,因此空间复杂度为O(n+logn).
基数排序
- 基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用
- 基数排序法是属于稳定性的排序,基数排序法的是效率高的稳定性排序法
- 基数排序(Radix Sort)是桶排序的扩展
- 基数排序是1887 年赫尔曼·何乐礼发明的。它是这样实现的:将整数按位数切割成不同的数字,然后按每个位数分别比较。
说明:
- 基数排序是对传统桶排序的扩展,速度很快.
- 基数排序是经典的空间换时间的方式,占用内存很大, 当对海量数据排序时,容易造成OutOfMemoryError 。
- 基数排序时稳定的。[注:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的]
- 有负数的数组,我们不用基数排序来进行排序。
public class RadixSort {
//基数排序
public static void radixSort(int[] arr) {
//取出数组的长度
int len = arr.length;
//构造桶 二维数组
int[][] bucket = new int[10][len];
//构造一个数组 用来记录每个桶里有多少个元素
int[] bucketElementCounts = new int[10];
//先求出数组中最大数的长度 以确定循环的层数是多少
int max = arr[0];
for (int i = 1; i < len; i++){
if (arr[i] > max){
max = arr[i];
}
}
//得到数组中最大的数为max,利用字符串的长度方法确定出最大数的位数
int maxLength = (max + "").length();
System.out.println("最大数长度为:"+ maxLength);
// 最外面一层表示桶排序 经历过几次
//流程:首先进入桶 然后从桶里取出来重新排序 这么循环下去就得到最后的排序
for (int i = 1, n = 1; i <= maxLength; i++, n *= 10){
//将元素装入桶中
for (int j = 0; j < len; j++){
//利用取模运算
int digitOfElement = (arr[j]/n) % 10;
// 这个相当于找到了要装入的桶
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
bucketElementCounts[digitOfElement]++;//因为角标是从零开始的 这个表示桶里面有多少个元素
}
//取完一轮后将桶中的数据重新排序并赋值给原来的数组
int index = 0;//辅助将值赋给数组的
for (int k = 0; k < 10; k++){//第几个桶
if (bucketElementCounts[k] != 0){
for (int l = 0; l < bucketElementCounts[k]; l++){
arr[index++] = bucket[k][l];
}
}
// 重新排序后将辅助记录每个桶有多少数据的数组置0
bucketElementCounts[k] = 0;
}
}
}
}
复杂度分析
基数排序的复杂度与最大数的长度有关,设最大数的长度为k,数组长度为n,则时间复杂度为O(n*k),空间复杂度为O(n+k)。由排序原理知道,此排序算法是稳定的。