在CSDN的第一个博客纪念,希望能越来越好
这里我们所叙述的为常见的十大排序算法,分别从原理、时间复杂度、空间复杂度以及具体实现为主
1.冒泡排序
1.1 算法思想
冒泡顾名思义就是想泡泡一样。轻的往上面漂,重的往下面去;排序方式为:一次比较两个元素,如果他们的顺序是错误的就将他们两个位置调换,所以经过一次排序可以确定一个元素的最终位置;
1.2 算法流程(按照升序的方式)
a. 从第一个位置开始遍历,比较相邻的元素,如果前一个元素比后面的元素大,则两者互换
b. 对每一对相邻的元素都进行这样的操作,从开始的第一对到最后的一对,这样最后的元素应该是这一组数据中最大的
c. 针对所有的元素都进行一次这样的操作,最后一个不需要,因为已经是最大的了
d. 重复以上a~c,直到排序完成。
下面是对应的动图
1.3 时间复杂度和空间复杂度问题
很容易分析出来:因为最差情况下(正好逆序),每一个元素都要进行排序,所以时间复杂度为;最好情况就是元素正好是顺序排列,只需要遍历一次就可以,时间复杂度为,所以综合之后还是为;空间负责度为
1.4 算法实现
public static void bubbleSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
// 如果左边的数大于右边的数,则交换,保证右边的数字最大
swap(arr, j, j + 1);
}
}
}
}
// 交换元素
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
2.选择排序
2.1 算法思想
双重循环遍历数组,每经过一轮比较,找到最小元素的下标,将其交换至首位。所以可以看出这里经过这些操作之后每一次也可以确定一个元素的最终位置。选择排序算是最稳定的排序算法了,因为不论输入数据如何排列,它均会遍历全部获得当时列表的最值。
2.2 算法流程
a. 假设输入数组为array[1...n],则此时有序区为空,无序区为array[0...n]
b. 当第i(i=1,2,3,...,n-1)趟排序时,当前有序区为array[1,...,i-1],无序区为array[i,...,n];此次从无序区里面选出最小值array[k],将它与无序区的第一个记录进行交换,使有序区为array[1,...,i],无序区为array[i+1,...,n]
c. 经过n-1次同样的操作之后,数组即有序化了
下图为动图分析:
1.3 时间复杂度和空间复杂度问题
时间复杂度:无论输入什么样的数据,该方法都要进行双层循环遍历,所以时间复杂度为
空间复杂度:使用有限个变量,空间复杂度 O(1)
1.4 算法实现:
public static void selectionSort(int[] arr){
int minIndex;
for (int i=0;i<arr.length-1;i++){
minIndex = i;
for (int j=i+1;j<arr.length;j++){
if (arr[minIndex] > arr[j]){
//记录最小值的下标
minIndex = j;
}
}
// 将最小元素交换至首位
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
3. 插入排序
3.1 算法思想
一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。生活中有一个很常见的场景:在打扑克牌时,我们一边抓牌一边给扑克牌排序,每次摸一张牌,就将它插入手上已有的牌中合适的位置,逐渐完成整个排序。
3.2 算法流程
a. 从第一个元素开始,该元素可以认为已经被排序
b. 取出下一个元素,在已经排序的元素序列中从后向前扫描
c. 如果该元素(已排序)大于新元素,将该元素移到下一位置
d. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
e. 将新元素插入到该位置后
f. 重复步骤b~e
动图分析:
3.3 时间复杂度和空间复杂度
时间复杂度:插入排序过程需要两层循环,时间复杂度为为
空间复杂度:使用有限个变量,空间复杂度 O(1)
3.4 算法实现
public static void insetrtSort(int[] arr){
//从第二个数开始,往前插入数字
for (int i=1;i<arr.length;i++){
int currentNumber = arr[i];
int j = i-1;
//寻找插入位置的过程中,不断将比currentNumber大的数组向后挪
while (j>=0 && currentNumber<arr[j]){
arr[j+1] =arr[j];
j--;
}
arr[j+1] = currentNumber;
}
}
4. 希尔排序
4.1 算法思想
希尔排序本质上是对插入排序的一种优化,它利用了插入排序的简单,又克服了插入排序每次只交换相邻两个元素的缺点。它的基本思想是:将待排序数组按照一定的间隔分为多个子数组,每组分别进行插入排序。这里按照间隔分组指的不是取连续的一段数组,而是每跳跃一定间隔取一个值组成一组,逐渐缩小间隔进行下一轮排序,最后一轮时,取间隔为 1,也就相当于直接使用插入排序。但这时经过前面的「宏观调控」,数组已经基本有序了,所以此时的插入排序只需进行少量交换便可完成。(引用于:LeetCode排序算法)
4.2 算法流程
a. 选择一个增量序列T1,T2,...,Tk
b. 按增量序列个数k,对序列进行k趟排序;
c. 每趟排序,根据对应的增量,将待排序列分割成若干长度为m的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个表作为一个单位进行排序
动图分析:
4.3 时间复杂度和空间复杂度
时间复杂度:希尔排序的复杂度实际上是介于O(1)到之间的,不过大多数情况下我们默认其时间复杂度为
空间复杂度:只需要常数级的临时变量,所以其空间复杂度为O(1)
4.4 算法实现
public static void shellSort(int[] arr){
for (int gap=arr.length/2;gap>0;gap/=2){
//分组,这里我们先以总长度的一半为分组长度的开始,每次减少一半最后到0
for (int groupStartIndex=0;groupStartIndex<gap;groupStartIndex++){
//插入排序
for (int currentIndex=groupStartIndex+gap;currentIndex<arr.length;currentIndex+=gap){
//currentNumber站起来开始找位置
int currentNumber = arr[currentIndex];
int preIndex = currentIndex-gap;
while (preIndex>=groupStartIndex && currentNumber<arr[preIndex]){
//向后挪位置
arr[preIndex+gap] = arr[preIndex];
preIndex -= gap;
}
arr[preIndex+gap] = currentNumber;
}
}
}
}
5. 归并排序
5.1 算法思想
很容易理解,之所以称之为归并,就是因为这是对两个列表或者数组进行归一的操作。将已有的有序子序列合并成一个有序序列。
5.2 算法流程
a. 把长度为n的输入序列分成两个长度为n/2的子序列;
b. 对这两个子序列分别采用归并排序;
c. 将两个排序好的子序列合并成一个最终的排序序列。
动图分析:
5.3 时间复杂度和空间复杂度
时间复杂度:拆分数组的过程中,会将数组拆分 logn 次,每层执行的比较次数都约等于 n 次,所以时间复杂度是 O(nlogn)。
空间复杂度:空间复杂度是 O(n),主要占用空间的就是我们在排序前创建的长度为n的结果数组。
5.4 算法实现(来源:LeetCode归并排序最优代码)
public static void mergeSort(int[] arr) {
if (arr.length == 0) return;
mergeSort(arr, 0, arr.length - 1);
}
// 对 arr 的 [start, end] 区间归并排序
private static void mergeSort(int[] arr, int start, int end) {
// 只剩下一个数字,停止拆分
if (start == end) return;
int middle = (start + end) / 2;
// 拆分左边区域
mergeSort(arr, start, middle);
// 拆分右边区域
mergeSort(arr, middle + 1, end);
// 合并左右区域
merge(arr, start, end);
}
// 将 arr 的 [start, middle] 和 [middle + 1, end] 区间合并
private static void merge(int[] arr, int start, int end) {
int end1 = (start + end) / 2;
int start2 = end1 + 1;
// 用来遍历数组的指针
int index1 = start;
while (index1 <= end1 && start2 <= end) {
if (arr[index1] > arr[start2]) {
// 将 index1 和 start2 下标的数字交换
exchange(arr, index1, start2);
if (start2 != end) {
// 调整交换到 start2 上的这个数字的位置,使右边区域继续保持有序
int value = arr[start2];
int index = start2;
// 右边区域比 arr[start2] 小的数字不断前移
while (index < end && arr[index + 1] < value) {
arr[index] = arr[index + 1];
index++;
}
// 交换到右边区域的这个数字找到了自己合适的位置,坐下
arr[index] = value;
}
}
index1++;
}
}
private static void exchange(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
6. 快速排序
6.1 算法思想
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据比另一部分的所有数据要小,再按这种方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,使整个数据变成有序序列。
6.2 算法流程
a. 从数组重挑选出一个数作为基准
b. 重新排列数组,凡是比基准小的元素都放置在基准前面,比基准大的元素都放在其后面,这样基准就把这个数组分成了两个区
c. 递归地把小于基准值元素的子数列和大于基准值元素的子数列排序
动图分析:
6.3 时间复杂度和空间复杂度
时间复杂度:快速排序在最坏的情况下时间复杂度为,不过这种情况并不常见,所以一般说他的平均时间复杂度:O(nlogn)
空间复杂度:速排序只是使用数组原本的空间进行排序,所以所占用的空间应该是常量级的,但是由于每次划分之后是递归调用,所以递归调用在运行的过程中会消耗一定的空间,一般情况下空间复杂度为O(logn),在最差的情况下,若每次只完成了一个元素,那么空间复杂度为O(n),
所以我们一般认为快速排序的空间复杂度为O(logn)
6.4 算法实现
public class QuickSort {
public static void main(String[] args) {
int[] arr = { 49, 38, 65, 97, 23, 22, 76, 1, 5, 8, 2, 0, -1, 22 };
quickSort(arr, 0, arr.length - 1);
System.out.println("排序后:");
for (int i : arr) {
System.out.println(i);
}
}
private static void quickSort(int[] arr, int low, int high) {
if (low < high) {
// 找寻基准数据的正确索引
int index = getIndex(arr, low, high);
// 进行迭代对index之前和之后的数组进行相同的操作使整个数组变成有序
//quickSort(arr, 0, index - 1); 之前的版本,这种姿势有很大的性能问题,谢谢大家的建议
quickSort(arr, low, index - 1);
quickSort(arr, index + 1, high);
}
}
private static int getIndex(int[] arr, int low, int high) {
// 基准数据
int tmp = arr[low];
while (low < high) {
// 当队尾的元素大于等于基准数据时,向前挪动high指针
while (low < high && arr[high] >= tmp) {
high--;
}
// 如果队尾元素小于tmp了,需要将其赋值给low
arr[low] = arr[high];
// 当队首元素小于等于tmp时,向前挪动low指针
while (low < high && arr[low] <= tmp) {
low++;
}
// 当队首元素大于tmp时,需要将其赋值给high
arr[high] = arr[low];
}
// 跳出循环时low和high相等,此时的low或high就是tmp的正确索引位置
// 由原理部分可以很清楚的知道low位置的值并不是tmp,所以需要将tmp赋值给arr[low]
arr[low] = tmp;
return low; // 返回tmp的正确位置
}
}
7. 堆排序
7.1 算法思想
通过堆这种数据结构,将数组中的值放入堆中,通过构造大顶堆和小顶堆的方式将数组排序
7.2 算法流程
a. 用数列构建出一个大顶堆,取出堆顶的数字;
b. 调整剩余的数字,构建出新的大顶堆,再次取出堆顶的数字;
c. 循环往复,完成整个排序。
动图如下(完整过程可以点击链接观看:初始化堆和交换元素):
7.3 时间复杂度和空间复杂度
时间复杂度:
堆排序分为两个阶段:初始化建堆(buildMaxHeap)和重建堆(maxHeapify,直译为大顶堆化)。所以时间复杂度要从这两个方面分析。根据数学运算可以推导出初始化建堆的时间复杂度为 O(n),重建堆的时间复杂度为 O(nlogn),所以堆排序总的时间复杂度为 O(nlogn)
空间复杂度:堆排序的空间复杂度为 O(1)O(1),只需要常数级的临时变量
7.4 算法实现
public static void heapSort(int[] arr){
//构建初始大顶堆
buildMaxHeap(arr);
for (int i=arr.length-1;i>=0;i--){
//将最大值放到数组最后
exchange(arr,0,i);
//调整剩余数组,使其满足大顶栈
maxHeapify(arr,0,i);
}
}
//构造初始大顶堆
public static void buildMaxHeap(int[] arr){
//从最后一个非叶子节点开始调整大顶栈,最后一个非叶子节点得下标就是Arr.length/2-1;
for (int i=arr.length/2-1;i>=0;i--){
maxHeapify(arr,i,arr.length);
}
}
//调整大顶堆,第三个参数表示剩余未排序得数字得数量,也就是剩余堆得大小
private static void maxHeapify(int[] arr,int i,int heapSize){
//左子节点下标
int l = 2*i + 1;
int r = l + 1; //右子节点下标
int largest = i; //记录根节点、左子树节点、右子树节点三者
//与左子树结点比较
if (l < heapSize && arr[l] > arr[largest]){
largest = l;
}
//与右子树节点比较
if (r<heapSize && arr[r]>arr[largest]){
largest = r;
}
if (largest != i){
//将最大值交换为根结点
exchange(arr,i,largest);
//再次调整交换数字后的大顶堆
maxHeapify(arr,largest,heapSize);
}
}
//交换元素
private static void exchange(int[] arr,int i,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
8. 计数排序
8.1 算法思想
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。计数排序(Counting sort)是一种稳定的排序算法。计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。它只能对整数进行排序
8.2 算法流程
a. 找出待排序的数组中最大和最小的元素;
b. 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
c. 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
d. 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
动图如下:
8.3 时间复杂度和空间复杂度
时间复杂度:时间复杂度为线性的排序算法,为O(n+k),k是待排序列最大值
空间复杂度:空间复杂度也为O(n+k),是一种空间换时间的方法
8.4 算法实现
private static int[] countSort(int[] array,int k)
{
int[] C=new int[k+1];//构造C数组
int length=array.length,sum=0;//获取A数组大小用于构造B数组
int[] B=new int[length];//构造B数组
for(int i=0;i<length;i++)
{
C[array[i]]+=1;// 统计A中各元素个数,存入C数组
}
for(int i=0;i<k+1;i++)//修改C数组
{
sum+=C[i];
C[i]=sum;
}
for(int i=length-1;i>=0;i--)//遍历A数组,构造B数组
{
B[C[array[i]]-1]=array[i];//将A中该元素放到排序后数组B中指定的位置
C[array[i]]--;//将C中该元素-1,方便存放下一个同样大小的元素
}
return B;//将排序好的数组返回,完成排序
}
9. 桶排序
9.1 算法思想
假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序
9.2 算法流程
a. 人为设置一个BucketSize,作为每个桶所能放置多少个不同数值(例如当BucketSize==5时,该桶可以存放{1,2,3,4,5}这几种数字,但是容量不限,即可以存放100个3);
b. 遍历输入数据,并且把数据一个一个放到对应的桶里去;
c. 对每个不是空的桶进行排序,可以使用其它排序方法,也可以递归使用桶排序;
d. 从不是空的桶里把排好序的数据拼接起来。
动图分析:
没有找到动图,所以这里放入流程图,可以阅读下面得网页:可视化桶排序算法
9.3 时间复杂度和空间复杂度
时间复杂度:桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
空间复杂度:桶排序的空间复杂度为O(N+M);N为待排序元素个数,M为桶的个数
9.4 算法实现(原文链接:java实现桶排序)
//bucketSize是初始化桶个数的一个基准值
public static int[] bucketSort(int[] arg,int bucketSize){
if(arg.length == 0){
return arg;
}
int maxValue = arg[0];
int minVaule = arg[0];
for (int i = 1; i < arg.length; i++) {
if(arg[i] < minVaule){
minVaule = arg[i];
} else if(arg[i] > maxValue){
maxValue = arg[i];
}
}
//桶的数量
int bucketCount = (int)Math.floor((maxValue - minVaule) / bucketSize) + 1;
//初始化一个二维数组,横坐标是桶的编号,纵坐标是值
int[][] buckets = new int[bucketCount][0];
//将待排序值按照一定规则映射到数组中
for (int i = 0; i < arg.length; i++) {
int index = (int)Math.floor((arg[i] - minVaule) / bucketSize);
buckets[index] = arrAppend(buckets[index], arg[i]);
}
int arrIndex = 0;
//循环每个桶
for (int i = 0; i < buckets.length; i++) {
if(buckets[i].length <= 0){
continue;
}
//将每个桶中的数组按照插入排序算法进行排序
int[] bucket = insertSort(buckets[i]);
//按照桶的顺序,将桶中排好序的值,依次放入到数组中
for (int j = 0; j < bucket.length; j++) {
arg[arrIndex++] = bucket[j];
}
}
return arg;
}
//插入排序算法
public static int[] insertSort(int[] arr){
for (int i = 1; i < arr.length; i++) {
// 记录要插入的数据
int tmp = arr[i];
// 从已经排序的序列最右边的开始比较,找到比其小的数
int j = i;
while (j > 0 && tmp < arr[j - 1]) {
arr[j] = arr[j - 1];
j--;
}
// 存在比其小的数,插入
if (j != i) {
arr[j] = tmp;
}
}
return arr;
}
//自动扩容,并保存数据
public static int[] arrAppend(int[] arr,int value){
arr = Arrays.copyOf(arr, arr.length + 1);
arr[arr.length - 1] = value;
return arr;
}
10. 基数排序
10.1 算法思想
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。
10.2 算法流程
a. 取得数组中的最大数,并取得位数;
b. arr为原始数组,从最低位开始取每个位组成radix数组;
c. 对radix进行计数排序(利用计数排序适用于小范围数的特点)
动图分析如下:
10.3 时间复杂度和空间复杂度
时间复杂度:最佳情况:T(n) = O(n * k) 最差情况:T(n) = O(n * k) 平均情况:T(n) = O(n * k)
空间复杂度:O(n)
10.4 算法实现(原文链接:基数排序java实现)
private static void radixSort(int[] array,int d)
{
int n=1;//代表位数对应的数:1,10,100...
int k=0;//保存每一位排序后的结果用于下一位的排序输入
int length=array.length;
int[][] bucket=new int[10][length];//排序桶用于保存每次排序后的结果,这一位上排序结果相同的数字放在同一个桶里
int[] order=new int[length];//用于保存每个桶里有多少个数字
while(n<d)
{
for(int num:array) //将数组array里的每个数字放在相应的桶里
{
int digit=(num/n)%10;
bucket[digit][order[digit]]=num;
order[digit]++;
}
for(int i=0;i<length;i++)//将前一个循环生成的桶里的数据覆盖到原数组中用于保存这一位的排序结果
{
if(order[i]!=0)//这个桶里有数据,从上到下遍历这个桶并将数据保存到原数组中
{
for(int j=0;j<order[i];j++)
{
array[k]=bucket[i][j];
k++;
}
}
order[i]=0;//将桶里计数器置0,用于下一次位排序
}
n*=10;
k=0;//将k置0,用于下一轮保存位排序结果
}
}