十大经典排序算法(动图演示)
算法概述
算法分类
十种常见排序算法可以分为两大类:
- 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
- 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
算法复杂度
相关概念
- 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
- 不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
- 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
- 空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。
冒泡排序(Bubble Sort)
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
算法描述
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;
- 重复步骤1~3,直到排序完成。
动图演示
代码实现
package cn.itcast.algorithm;
import java.util.Arrays;
public class BubbleSort {
public static void main(String[] args) {
int arr[] = {119, 34, 1, 101};
bubbleSort(arr);
System.out.println("排序后:" + Arrays.toString(arr));
}
public static void bubbleSort(int[] arr) {
int temp = 0;//临时变量
boolean flag = false;//标识变量,表示是否进行过交换
for (int i = 0; i < arr.length - 1; i++) {//遍历数组长度-1次
for (int j = 0; j < arr.length - 1 - i; j++) {//j < arr.length - 1 - i (-i是因为每遍历一趟,确定下来一个值,也就是循环的值每次-i)
//如果前面的数比后面的数大,则交换
if (arr[j] > arr[j + 1]) {
flag = true;//表示进行过交换
//交换
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
System.out.println("第" + (i + 1) + "趟排序后的数组");//(i+1)因为i是从0开始的
if (!flag) {//在一趟排序中,一次交换都没有发生过
break;
} else {
flag = false;//重置flag,进行下次判断
}
}
}
}
代码优化:
package cn.itcast.algorithm;
import java.util.Arrays;
public class BubbleSort {
public static void main(String[] args) {
int arr[] = {119, 34, 1, 101};
bubbleSort(arr);
System.out.println("排序后:" + Arrays.toString(arr));
}
public static void bubbleSort(int[] arr) {
int temp = 0;//临时变量
for (int i = 0; i < arr.length - 1; i++) {//遍历数组长度-1次
boolean flag = false;//标识变量,表示是否进行过交换
for (int j = 0; j < arr.length - 1 - i; j++) {//j < arr.length - 1 - i (-i是因为每遍历一趟,确定下来一个值,也就是循环的值每次-i)
//如果前面的数比后面的数大,则交换
if (arr[j] > arr[j + 1]) {
flag = true;//表示进行过交换
//交换
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
if (!flag) {//在一趟排序中,一次交换都没有发生过,if里本身要是true才会往下执行
break;//跳出最近的循环
}
}
}
}
选择排序(Selection Sort)
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
算法描述
n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:
- 初始状态:无序区为R[1..n],有序区为空;
- 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。
该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区 和 记录个数减少1个的新无序区;
- n-1趟结束,数组有序化了。
动图演示
代码实现
package cn.itcast.algorithm;
import java.util.Arrays;
public class SelectSort {
public static void main(String[] args) {
int arr[] = {119, 34, 1, 101};
selectSort(arr);
System.out.println("排序后:" + Arrays.toString(arr));
}
public static void selectSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {//遍历长度-1次
int minIndex = i;
int min = arr[i];
for (int j = i + 1; j < arr.length; j++) {
if (min > arr[j]) {//假定的最小值,不一定是最小
min = arr[j];//重置min,并不改变arr[]数组内的值
minIndex = j;//重置minIndex
}
}
if (minIndex != i) {//最小值下标不是i,表示最小值不是它自己,则进行下面的交换
arr[minIndex] = arr[i];//将当前轮下标i(当前轮次第一个)对应的值赋给最小值对应的下标的值,覆盖原来的值
arr[i] = min;//让之前拿到的最小值min赋给最小值的当前轮下标i对应的值
}
}
}
}
插入排序(Insertion Sort)
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
算法描述
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
动图演示
package cn.itcast.algorithm;
import java.util.Arrays;
public class InsertSort {
public static void main(String[] args) {
int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
System.out.println(Arrays.toString(arr));
insertSort(arr);
}
public static void insertSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {//不需要-1,因为i是从1开始的
//定义待插入的数,先假定一个是有序的
int insertVal = arr[i];
//待插入数前面那个数的下标
int insertIndex = i - 1;
//数组不越界 并且 待插入的数小于待插入前面那个数(还没有找到插入位置)
while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
arr[insertIndex + 1] = arr[insertIndex];//将待插入数前面那个数 覆盖 它后面那个数(arr[indexIndex]后移)
insertIndex--;//为了找插入位置,不断往前遍历
}
arr[insertIndex + 1] = insertVal;//把之前存起来要插入的数插入到对应位置。 insertIndex+1:顺序正确时,+1保持值不变 | 顺序不正确时(已经进了while循环减过1了):+1后就是要插入的位置
}
}
}
希尔排序(Shell Sort)
简单插入排序,当需要插入的数是较小的数时,后移的次数明显增多,对效率有影响.
1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
算法描述
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 按增量序列个数k,对序列进行k 趟排序;
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
当需要插入的数是较小的数时,后移的次数明显增多,对效率有影响.
动图演示
代码实现
package cn.itcast.algorithm;
import java.util.Arrays;
public class ShellSort {
public static void main(String[] args) {
int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
shallSort2(arr);
System.out.println(Arrays.toString(arr));
}
public static void shallSort2(int[] arr) {
//增量gap,并逐步的缩小增量
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
//从第gap个元素,逐个对其所在的组进行直接插入排序
for (int i = gap; i < arr.length; i++) {
int j = i;//待插入位置的下标
int temp = arr[j];//临时变量记录要插入的数
//如果待插入的数 小于 自己组里待插入数之前的数
if (arr[j] < arr[j - gap]) {
//进行插入排序
while (j - gap >= 0 && temp < arr[j - gap]) {//j-gap:是因为每个组里都有初始增量gap,最外层for循环每次遍历,gap都会改变
//移动
arr[j] = arr[j - gap];
j -= gap;
}
//当退出while循环后,就给temp找到了位置
arr[j] = temp;
}
}
}
}
}
快速排序(Quick Sort)
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
算法描述
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
动图演示
代码实现
package cn.itcast.algorithm;
import java.util.Arrays;
public class QuickSort {
public static void main(String[] args) {
int[] arr = {-9, 78, 6, 23, -30, 70};
QuickSort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
public static void QuickSort(int[] arr, int left, int right) {
int l = left;//左下标
int r = right;//右下标
//中轴的值
int pivot = arr[(l + r) / 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>=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 += 1;
r -= 1;
}
//向左递归
if (left < r) {
QuickSort(arr, left, r);
}
//向右递归
if (right > l) {
QuickSort(arr, l, right);
}
}
}
归并排序(Merge Sort)
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
算法描述
- 把长度为n的输入序列分成两个长度为n/2的子序列;
- 对这两个子序列分别采用归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
动图演示
代码实现
package cn.itcast.algorithm;
import java.util.Arrays;
public class MergeSort {
public static void main(String[] args) {
int arr[] = {8, 4, 5, 7, 1, 3, 6, 2};
int temp[] = new int[arr.length];
mergeSort(arr, 0, arr.length - 1, temp);
System.out.println(Arrays.toString(arr));
}
//分+合方法
public static void mergeSort(int[] arr, int left, int right, int[] temp) {
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;//中间索引
int t = 0;//指向temp的中间索引
//(-)、先把左右两边有序的数据按照规则填充到temp数组,直到左右两边的有序序列,有一边处理完毕为止
while (i <= mid && j <= right) {
//如果左边的有序序列的当前元素小于等于右边有序序列的当前元素
//即将左边有序序列的当前元素填充到temp数组
//然后t++,i++
if (arr[i] <= arr[j]) {
temp[t] = arr[i];
t += 1;
i += 1;
//反之,将右边有序序列的当前元素填充到temp数组
} else {
temp[t] = arr[j];
t += 1;
j += 1;
}
}
//(二)、把有剩余元素的一边的数据依次全部填充到temp数组中
//左边的有序序列还有剩余的元素,就全部填充到temp数组
while (i <= mid) {
temp[t] = arr[i];
t += 1;
i += 1;
}
//右边的有序序列还有剩余的元素,就全部填充到temp数组
while (j <= right) {
temp[t] = arr[j];
t += 1;
j += 1;
}
//(三)、将temp数组里的有序元素拷贝回arr数组
//从左边开始拷贝, 注意:不是每次都拷贝所有
t = 0;
int tempLeft = left;
//第一次合并:templeft = 0,right = 1。 第二次合并:templeft = 2,right = 3。 最后一次:templeft = 0,right = 7
while (tempLeft <= right) {
arr[tempLeft] = temp[t];
t += 1;
tempLeft += 1;
}
}
}
基数排序(Radix Sort)
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
算法描述
- 取得数组中的最大数,并取得位数;
- arr为原始数组,从最低位开始取每个位组成radix数组;
- 对radix进行计数排序(计数排序就是计算在当前一维数组有多少个数据,利用计数排序适用于小范围数的特点);
动图演示
代码实现package cn.itcast.algorithm;
import java.util.Arrays; /** * 典型的空间换时间的排序算法 */ public class RadixSort { public static void main(String[] args) { int arr[] = {53, 3, 542, 748, 14, 214}; radixSort(arr); } public static void radixSort(int[] arr) { //得到数组中最大数 int max = arr[0];//假设第一个数就是最大数 for (int i = 1; i < arr.length; i++) { if (arr[i] > max) { max = arr[i]; } } //得到最大数是几位数 int maxLength = (max + "").length(); //二维数组包含10个一维数组,每个一维数组大小只能为arr.length(空间换时间) int[][] bucket = new int[10][arr.length]; //定义一个一维数组来记录各个桶放入的数据个数, bucketElementCounts[0],记录的就是bucket[0]这个桶放入的数据个数 int[] bucketElementCounts = new int[10]; for (int i = 0, n = 1; i < maxLength; i++, n *= 10) { for (int j = 0; j < arr.length; j++) { //个位的值,也是对应桶下标的值 int digitOfElement = arr[j] / n % 10; //放入对应的桶中, bucketElementCounts[digitOfElement]为(digitOfElement例如值为第3个桶)里数的个数
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j]; bucketElementCounts[digitOfElement]++; } //按照这个桶的顺序(一维数组的下标依次取出,放回原数组) int index = 0; //遍历每一个桶,并将桶中的数据放入原数组 //bucketElementCounts.length = 10 for (int k = 0; k < bucketElementCounts.length; k++) { // !0说明有数据,才将数据放入原数组,bucketElementCounts[k]为第k个桶 if (bucketElementCounts[k] != 0) { //循环该桶,即第k个桶(第k个一维数组),放入 for (int l = 0; l < bucketElementCounts[k]; l++) { //取出元素放入arr中 arr[index] = bucket[k][l]; index++; } } //一轮过后,需要将bucketElementCounts[k]=0; bucketElementCounts[k] = 0; } System.out.println("第" + (i+1) + "次,取个位 " + Arrays.toString(arr)); } } }
bucketElementCounts[digitOfElement]为(digitOfElement)里数的个数