冒泡排序
冒泡排序(Bubble sort)是一种简单的排序算法。它重复地遍历要排序的序列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。遍历序列的工作重复地进行直到没有再需要交换,也就是说该序列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到序列的顶端。
算法步骤:
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 重复上面的步骤,直到排序完成。
算法分析:
- 时间复杂度: 最佳:O(n),最差:O(n2),平均:O(n2)
- 空间复杂度: O(1)
- 稳定性:稳定
- 排序方式:In-place
代码实现:
/**
* 冒泡排序
* @param nums
* @return nums
*/
public static int[] bubbleSort(int[] nums){
for(int i=0;i<nums.length-1;i++){
boolean flag = true;
for(int j=0;j<nums.length-1-i;j++){
if(nums[j] > nums[j+1]){
int tmp = nums[j];
nums[j] = nums[j+1];
nums[j+1] = tmp;
flag = false;
}
}
if(flag){
break;
}
}
return nums;
}
选择排序
选择排序(Selection sort)是一种简单直观的排序算法。 它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。 以此类推,直到全部待排序的数据元素的个数为零。
选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对n个元素的表进行排序总共进行至多(n-1)次交换。
算法步骤:
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第 2 步,直到所有元素均排序完毕。
算法分析:
- 时间复杂度: 最佳:O(n2),最差:O(n2),平均:O(n2)
- 空间复杂度: O(1)
- 稳定性:不稳定
- 排序方式:In-place
代码实现:
/**
* 选择排序
* @param nums
* @return nums
*/
public static int[] selectionSort(int[] nums){
for(int i=0;i<nums.length-2;i++){
int minIndex = i;
for(int j=i+1;j<nums.length;j++){
if(nums[j] < nums[minIndex]){
minIndex = j;
}
}
if(minIndex != i){
int tmp = nums[i];
nums[i] = nums[minIndex];
nums[minIndex] = tmp;
}
}
return nums;
}
插入排序
插入排序(Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序,因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
算法步骤:
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果该元素(已排序)大于新元素,将该元素移到下一位置
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置后
- 重复步骤2~5
算法分析:
- 时间复杂度: 最佳:O(n),最差:O(n2),平均:O(n2)
- 空间复杂度: O(1)
- 稳定性:稳定
- 排序方式:In-place
代码实现:
/**
* 插入排序
* @param nums
* @return nums
*/
public static int[] insertionSort(int[] nums){
for(int i=1;i<nums.length;i++){
int index = i-1;
int current = nums[i];
while (index>=0 && nums[index] > current){
nums[index+1] = nums[index];
index--;
}
nums[index+1] = current;
}
return nums;
}
希尔排序
希尔排序(Shell sort)也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为递减增量排序算法,同时该算法是冲破
O(n²)
的第一批算法之一。
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录 “基本有序” 时,再对全体记录进行依次直接插入排序。
算法步骤:
- 选择一个增量序列
{t1, t2, …, tk}
,其中(ti>tj, i<j, tk=1)
; - 按增量序列个数 k,对序列进行 k 趟排序;
- 每趟排序,根据对应的增量
t
,将待排序列分割成若干长度为m
的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
⚠️增量序列的选择:希尔排序的增量序列的选择与证明是个数学难题,一般的初次取序列的一半为增量,以后每次减半,直到增量为1,这是希尔建议的增量,称为希尔增量{n/2, (n/2)/2, ..., 1}
,但其实这个增量序列不是最优的。
算法分析:
- 时间复杂度: 最佳:O(nlogn),最差:O(n2),平均:O(nlogn)
- 空间复杂度: O(1)
- 稳定性:稳定
- 排序方式:In-place
代码实现:
/**
* 希尔排序
* @param nums
* @return
*/
public static int[] shellSort(int[] nums){
int gap = nums.length/2;
while(gap > 0){
for(int i=gap;i<nums.length;i++){
int index = i-gap;
int current = nums[i];
while (index>=0 && nums[index]>current){
nums[index+gap] = nums[index];
index -= gap;
}
nums[index+gap] = current;
}
gap /= 2;
}
return nums;
}
归并排序
归并排序(Merge sort)是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。 将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。 若将两个有序表合并成一个有序表,称为二路归并。
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)
的时间复杂度。代价是需要额外的内存空间
算法步骤:
归并排序算法是一个递归过程,边界条件为当输入序列仅有一个元素时,直接返回,具体过程如下:
- 如果输入内只有一个元素,则直接返回,否则将长度为
n
的输入序列分成两个长度为n/2
的子序列; - 分别对这两个子序列进行归并排序,使子序列变为有序状态;
- 设定两个指针,分别指向两个已经排序子序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间(用于存放排序结果),并移动指针到下一位置;
- 重复步骤 3 ~4 直到某一指针达到序列尾;
- 将另一序列剩下的所有元素直接复制到合并序列尾。
算法分析:
- 时间复杂度: 最佳:O(nlogn),最差:O(nlogn),平均:O(nlogn)
- 空间复杂度: O(n)
- 稳定性:稳定
- 排序方式:Out-place
代码实现:
/**
* 归并排序
* @param nums
* @return nums
*/
public static int[] mergeSort(int[] nums){
if(nums.length <= 1){
return nums;
}
int mid = nums.length/2;
int[] nums_1 = Arrays.copyOfRange(nums,0,mid);
int[] nums_2 = Arrays.copyOfRange(nums,mid,nums.length);
return merge(mergeSort(nums_1),mergeSort(nums_2));
}
/**
* 归并两个有序序列
* @param nums_1
* @param nums_2
* @return sorted_nums
*/
public static int[] merge(int[] nums_1,int[] nums_2){
int[] sorted_nums = new int[nums_1.length+nums_2.length];
int idx = 0,idx_1 = 0,idx_2 = 0;
while (idx_1 < nums_1.length && idx_2 < nums_2.length){
if(nums_1[idx_1] < nums_2[idx_2]){
sorted_nums[idx++] = nums_1[idx_1++];
}else{
sorted_nums[idx++] = nums_2[idx_2++];
}
}
while(idx_1 < nums_1.length){
sorted_nums[idx++] = nums_1[idx_1++];
}
while (idx_2 < nums_2.length){
sorted_nums[idx++] = nums_2[idx_2++];
}
return sorted_nums;
}
快速排序
快速排序(Quicksort),又称分区交换排序(partition-exchange sort),简称快排,一种排序算法。在平均状况下,排序n个元素要 O(nlogn)次比较。在最坏状况下则需要 O(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他算法更快,因为它的内部循环可以在大部分的架构上很有效率地达成。
快速排序的主要思想是通过划分将待排序的序列分成前后两部分,其中前一部分的数据都比后一部分的数据要小,然后再递归调用函数对两部分的序列分别进行快速排序,以此使整个序列达到有序。
基本思想:
- 从待排序的序列中随机的选择一个数字作为pivot中心轴
- 将小于pivot的数放在pivot的左边
- 将大于pivot的数放在pivot的右边
- 分别对左右子序列重复前三步操作
算法步骤:
- 首先从序列中随机选择一个数作为pivot中心轴,通常为了方便,直接选择第一个数;
- 定义两个变量left和right,分别指向序列中的第一个元素和最后一个元素;
- 先从右到左找一个小于pivot中心轴的数,将其放在left所指位置;然后从左到右找一个大于pivot中心轴的数,将其放在right所指位置;
- 重复步骤3,直至left=right;然后将pivot中心轴数放在left与right共同指向的那个位置,这时,pivot中心轴数已经归位;
- 分别对左右子序列重复上述操作,直至最后整个序列有序。
算法分析:
- 时间复杂度: 最佳:O(nlogn),最差:O(n2),平均:O(nlogn)
- 空间复杂度: O(logn)
- 稳定性:不稳定
- 排序方式:In-place
代码实现:
/**
* 快速排序
* @param nums
* @param start
* @param end
*/
public static int[] quickSort(int[] nums, int start, int end) {
if (start >= end) return nums;
int left = start, right = end;
int pivot = nums[start];
while (left < right) {
while (left < right && nums[right] >= pivot) {
right--;
}
if (left < right) {
nums[left] = nums[right];
}
while (left < right && nums[left] <= pivot) {
left++;
}
if (left < right) {
nums[right] = nums[left];
}
}
nums[left] = pivot;
quickSort(nums, start, left - 1);
quickSort(nums, left + 1, end);
return nums;
}
堆排序
堆排序(Heap sort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆的性质:即子结点的值总是小于(或者大于)它的父节点。
基本思想:先把数组构造成一个大顶堆(父亲节点大于其子节点),然后把堆顶(数组最大值,数组第一个元素)和数组最后一个元素交换,这样就把最大值放在了数组最后边。把数组长度减1,再进行构造堆,把剩余的第二大值放到堆顶,把堆顶元素和剩余未排序数组的最后一个元素交换。依此类推,直至数组排序完成。
算法步骤:
- 将初始待排序列
(R1, R2, ……, Rn)
构建成大顶堆,此堆为初始的无序区; - 将堆顶元素
R[1]
与最后一个元素R[n]
交换,此时得到新的无序区(R1, R2, ……, Rn-1)
和新的有序区 (Rn), 且满足R[1, 2, ……, n-1]<=R[n]
; - 由于交换后新的堆顶
R[1]
可能违反堆的性质,因此需要对当前无序区(R1, R2, ……, Rn-1)
调整为新堆,然后再次将 R [1] 与无序区最后一个元素交换,得到新的无序区(R1, R2, ……, Rn-2)
和新的有序区(Rn-1, Rn)
。不断重复此过程直到有序区的元素个数为n-1
,则整个排序过程完成。
算法分析:
- 时间复杂度: 最佳:O(nlogn),最差:O(nlogn),平均:O(nlogn)
- 空间复杂度: O(1)
- 稳定性:不稳定
- 排序方式:In-place
代码实现:
/**
* 堆排序
* @param nums
* @return nums
*/
static int heapLen;
public static int[] heapSort(int[] nums){
heapLen = nums.length;
buildMaxHeap(nums);
for(int i = nums.length-1;i>0;i--){
swap(nums, i, 0);
heapLen--;
heapify(nums,0);
}
return nums;
}
/**
* 交换两个数
* @param nums
* @param i
* @param j
*/
private static void swap(int[] nums,int i,int j){
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
/**
* 构造最大堆
* @param nums
*/
private static void buildMaxHeap(int[] nums){
for(int i= (nums.length-1)/2;i>=0;i--) {
heapify(nums, i);
}
}
/**
* 调整最大堆
* @param nums
* @param i
*/
private static void heapify(int[] nums,int i){
int left = 2*i+1;
int right = 2*i+2;
int largest = i;
if(right < heapLen && nums[right] > nums[largest]){
largest = right;
}
if(left < heapLen && nums[left] > nums[largest]){
largest = left;
}
if(largest != i){
swap(nums,i,largest);
heapify(nums,largest);
}
}
计数排序
计数排序(Counting sort)是一种稳定的线性时间排序算法,使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。计数排序要求输入的数据必须是有确定范围的整数。
当序列中有相同元素时,需要反向填充结果数组。当输入的元素是n
个0
到k
之间的整数时,它的运行时间是O(n+k)
。计数排序不是比较排序,排序的速度快于任何比较排序算法。由于用来计数的数组C
的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上 1),这使得计数排序对于数据范围很大的数组,需要大量额外内存空间。
算法步骤:
- 找出数组中的最大值
max
、最小值min
; - 创建一个新数组
C
,其长度是max-min+1
,其元素默认值都为 0; - 遍历原数组
A
中的元素A[i]
,以A[i]-min
作为C
数组的索引,以A[i]
的值在A
中元素出现次数作为C[A[i]-min]
的值; - 对
C
数组变形,新元素的值是该元素与前一个元素值的和,即当i>1
时C[i] = C[i] + C[i-1]
; - 创建结果数组
R
,长度和原始数组一样。 - 从后向前遍历原始数组
A
中的元素A[i]
,使用A[i]
减去最小值min
作为索引,在计数数组C
中找到对应的值C[A[i]-min]
,C[A[i]-min]-1
就是A[i]
在结果数组R
中的位置,做完上述这些操作,将count[A[i]-min]
减小 1。(注意:从后向前遍历是为了保证算法的稳定性,将count[A[i]-min]
减小 1可以处理序列中有相同元素的情况)
算法分析:
- 时间复杂度: 最佳:O(n+k),最差:O(n+k),平均:O(n+k)
- 空间复杂度: O(k)
- 稳定性:稳定
- 排序方式:Out-place
代码实现:
/**
* 计数排序
* @param nums
* @return
*/
public static int[] countingSort(int[] nums){
if(nums.length < 2) return nums;
int[] minAndMax = getMinAndMax(nums);
int min = minAndMax[0];
int max = minAndMax[1];
int[] countArr = new int[max-min+1];
int[] result = new int[nums.length];
for(int i=0;i<nums.length;i++){
countArr[nums[i]-min]++;
}
for(int i=1;i<countArr.length;i++){
countArr[i] = countArr[i] + countArr[i-1];
}
for(int i=nums.length-1;i>=0;i--){
int idx = countArr[nums[i]-min]-1;
result[idx] = nums[i];
countArr[nums[i]-min]--;
}
return result;
}
/**
* 获取序列的最小值和最大值
* @param nums
* @return
*/
private static int[] getMinAndMax(int[] nums){
int min = nums[0];
int max = nums[0];
for(int i=1;i<nums.length;i++){
if(nums[i] < min){
min = nums[i];
}else if(nums[i] > max){
max = nums[i];
}
}
return new int[]{min,max};
}
桶排序
桶排序(Bucket sort)或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶子里。 每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。 桶排序是鸽巢排序的一种归纳结果。 当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间 O(n)。
算法步骤:
- 设置一个 BucketSize,作为每个桶所能放置多少个不同数值;
- 遍历输入数据,并且把数据依次映射到对应的桶里去;
- 对每个非空的桶进行排序,可以使用其它排序方法,也可以递归使用桶排序;
- 从非空桶里把排好序的数据拼接起来。
算法分析:
- 时间复杂度: 最佳:O(n+k),最差:O(n2),平均:O(n+k)
- 空间复杂度: O(k)
- 稳定性:稳定
- 排序方式:Out-place
代码实现:
/**
* 桶排序
* @param nums
* @param bucketSize
* @return
*/
public static int[] bucketSort(int[] nums,int bucketSize){
if(nums.length<2 || bucketSize==0) return nums;
int[] minAndMax = getMinAndMax(nums);
int min = minAndMax[0];
int max = minAndMax[1];
// 桶的数量及初始化
int bucketCnt = (max-min)/bucketSize+1;
List<List<Integer>> buckets = new ArrayList<>();
for(int i=0;i<bucketCnt;i++){
buckets.add(new ArrayList<Integer>());
}
// 把数据依次映射到对应的桶中去
for(int num:nums){
int idx = (num-min)/bucketSize;
buckets.get(idx).add(num);
}
// 对每个非空的桶进行排序
for(int i=0;i<buckets.size();i++){
if(buckets.get(i).size() > 1){
buckets.get(i).sort(Comparator.naturalOrder());
}
}
// 从非空桶里把排好序的序列拼接起来
int[] result = new int[nums.length];
int idx = 0;
for(List<Integer> bucket:buckets){
for(int num:bucket){
result[idx++] = num;
}
}
return result;
}
/**
* 获取序列的最小值和最大值
* @param nums
* @return
*/
private static int[] getMinAndMax(int[] nums){
int min = nums[0];
int max = nums[0];
for(int i=1;i<nums.length;i++){
if(nums[i] < min){
min = nums[i];
}else if(nums[i] > max){
max = nums[i];
}
}
return new int[]{min,max};
}
基数排序
基数排序(Radix sort)是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
算法步骤:
- 取得数组中的最大数,并取得位数,即为迭代次数
N
(例如:数组中最大数值为 1000,则N=4
); A
为原始数组,从最低位开始取每个位组成radix
数组;- 对
radix
进行计数排序(利用计数排序适用于小范围数的特点); - 将
radix
依次赋值给原数组; - 重复 2~4 步骤
N
次
算法分析:
- 时间复杂度: 最佳:O(n×k),最差:O(n×k),平均:O(n×k)
- 空间复杂度: O(n+k)
- 稳定性:稳定
- 排序方式:Out-place
代码实现:
/**
* 基数排序
* @param nums
* @return
*/
public static int[] radixSort(int[] nums){
if(nums.length < 2) return nums;
int max = nums[0];
for(int i=1;i<nums.length;i++){
if(nums[i] > max)
max = nums[i];
}
int N = 0;
while(max > 0){
max/=10;
N++;
}
for(int i=0;i<N;i++){
List<List<Integer>> radix = new ArrayList<>();
for(int j=0;j<10;j++){
radix.add(new ArrayList<Integer>());
}
for(int num:nums){
int idx = (num/(int)Math.pow(10,i))%10;
radix.get(idx).add(num);
}
int idx = 0;
for(List<Integer> r:radix){
for(int num:r){
nums[idx++] = num;
}
}
}
return nums;
}
计数排序 vs 桶排序 vs 基数排序
相同点:
- 都利用了桶的概念
- 都是稳定的
- 都是非比较类排序算法
不同点:
- 计数排序:每个桶只存储单一键值
- 桶排序:每个桶存储一定范围的数值
- 基数排序:根据键值的每位数字来分配桶