排序算法
- 前言
- 一、插入排序法
- 二、冒泡排序
- 三、选择排序
- 四、希尔排序
- 五、归并排序
- 六、快速排序
- 七、堆排序
- 八、计数排序
- 九、桶排序
- 十、基数排序
- 总结
- 补充小数和问题(归并排序思想)
- 问题描述
- 解法
前言
本文主要介绍几种排序算法的java实现极其时间复杂度、空间负责度以及其稳定性。 同时对其应用做简要介绍。
一、插入排序法
- 基本思想:假定有数组a,我们假定前n-1位元素有序,那么我们在排第n位元素时,只需要另第n位元素与前n-1位元素依次做比较,如果a[j] (j>=0 && j<=n-1),那么a[j]中的元素就后移一位即a[j+1]=a[j],直到出现第一个小于等于a[n]元素那么停止比较并使a[j+1] = a[n]
- 时间复杂度:O(N2)
- 空间复杂度:O(1)
- 稳定性:根据对相等状况的处理算法可以稳定也可以不稳定。如果出现相等状况则有序元素后移那么则不稳定,否则稳定。
package Sorted;
import java.util.Arrays;
public class InsertSort {
public static void insertSort(int[] a) {
int len = a.length;
for(int i=1;i<len;++i) {
int temp = a[i]; //用于暂时存放待排序元素
int j=i-1;
for(;j>=0;--j) { //与前面的已排序数组作比较
if(temp < a[j]) {
a[j+1] = a[j]; //大数后移
}else { //出现小于等于的数字,找到了插入位置需要跳出循环(稳定)
break;
}
}
a[j+1] = temp; //插入到第一个小于等于temp的元素后面
}
}
public static void main(String[] args) {
int[] nums = new int[30];
for(int i=0;i<nums.length;++i) {
nums[i] = (int)(Math.random() * 100); //随机产生30个数字
}
System.out.println(Arrays.toString(nums));
insertSort(nums);
System.out.println(Arrays.toString(nums));
}
}
二、冒泡排序
- 基本思想:它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
- 时间复杂度:O(N2)
- 空间复杂度:O(1)
- 稳定性:稳定
package Sorted;
import java.util.Arrays;
public class BubbleSort {
public static void bubbleSort(int[] a) {
int len = a.length;
for(int i=0; i<len; ++i) { //排n-1轮,每次排好一个元素排完n-1个数组就有序了
int flag = 0; //标记是否发生了交换
for(int j=len-1;j>i;--j) { //从底部向上冒
if(a[j] < a[j-1]) { //如果a[j]小则交换最后最小的元素将在上面
flag = 1; //发生了交换
int temp = a[j];
a[j] = a[j-1];
a[j-1] = temp;
}
}
if(flag == 0) break; //如果未发生交换则顺序已排好
}
}
public static void main(String[] args) {
int[] nums = new int[30];
for(int i=0;i<nums.length;++i) {
nums[i] = (int)(Math.random() * 100); //随机产生30个数字
}
System.out.println(Arrays.toString(nums));
bubbleSort(nums);
System.out.println(Arrays.toString(nums));
}
}
三、选择排序
- 基本思想:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
- 时间复杂度:O(N2)
- 空间复杂度:O(1)
- 稳定性:不稳定
package Sorted;
import java.util.Arrays;
public class SelectionSort {
public static void selectionSort(int[] nums) {
if(nums.length <= 1) return ;
for(int i=0; i<nums.length - 1; ++i) { //每次排好一个元素排好n-1各元素之后就有序了
int min = i;
for(int j=i+1; j<nums.length; ++j) {
if(nums[min] > nums[j]) {
min = j;
}
}
int temp = nums[min]; //将未排序的最小元素移到前面
nums[min] = nums[i];
nums[i] = temp;
}
}
public static void main(String[] args) {
int[] nums = new int[30];
for(int i=0;i<nums.length;++i) {
nums[i] = (int)(Math.random() * 100); //随机产生30个数字
}
System.out.println(Arrays.toString(nums));
selectionSort(nums);
System.out.println(Arrays.toString(nums));
}
}
四、希尔排序
- 基本思想: 希尔排序是把记录按下表的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
- 时间复杂度:O(nlogn)
- 空间复杂度:O(1)
- 稳定性:不稳定
package Sorted;
import java.util.Arrays;
public class ShellSort {
public static void shellSort(int[] nums) {
int len = nums.length;
for(int r = nums.length/2; r>0; r=r/2) { //分组
for(int j=r; j<len; ++j) { //从每组的第二个数开始进行直接插入排序法
int preIndex = j - r;
int temp = nums[j];
while(preIndex >= 0 && nums[preIndex] > temp) {
nums[preIndex + r] = nums[preIndex];
preIndex -= r;
}
nums[preIndex + r] = temp;
}
}
}
public static void main(String[] args) {
int[] nums = new int[30];
for(int i=0;i<nums.length;++i) {
nums[i] = (int)(Math.random() * 100); //随机产生30个数字
}
System.out.println(Arrays.toString(nums));
shellSort(nums);
System.out.println(Arrays.toString(nums));
}
}
五、归并排序
- 基本思想: 该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
- 时间复杂度:O(nlogn)
- 空间复杂度:O(n)
- 稳定性:稳定
package Sorted;
import java.util.Arrays;
public class MergeSort {
public static void mergeSort(int[] nums, int l, int r) {
if(r - l == 1) {
return;
}
int mid = l + ((r - l) >> 1);
mergeSort(nums, l, mid); //使得左部分有序
mergeSort(nums, mid, r); //使得右部分有序
merge(nums, l, mid, r); //合并
}
public static void merge(int[] nums, int l, int mid, int r) {
int[] temp = new int[r - l];
int k = 0; // temp指针
int il= l; //左数组指针
int ir= mid;//右数组指针
while(il < mid && ir < r) { //对左右部分进行比较较小的数拷贝,指针后移
if(nums[il] <= nums[ir]) {
temp[k++] = nums[il++];
}else {
temp[k++] = nums[ir++];
}
}
while(il < mid) { //对于剩余部分无需比较直接填充即可
temp[k++] = nums[il++];
}
while(ir < r) {
temp[k++] = nums[ir++];
}
k = 0;
while(k<temp.length) {
nums[l++] = temp[k++];
}
}
public static void main(String[] args) {
int[] nums = new int[30];
for(int i=0;i<nums.length;++i) {
nums[i] = (int)(Math.random() * 100); //随机产生30个数字
}
int[] nums2 = Arrays.copyOfRange(nums, 0, nums.length);
System.out.println(Arrays.toString(nums));
mergeSort(nums, 0, nums.length);
System.out.println(Arrays.toString(nums));
Arrays.sort(nums2);
System.out.println(Arrays.toString(nums2));
}
}
六、快速排序
- 基本思想: 通过一趟排序将待排记录分隔成独立的三部分,一部分比所选的基数小,一部分比所选的基数大,一部分等于所选的基数。则所选的基数已经在正确的位置上了(等于部分),之后只需对另外两部分再执行该操作即可,直至每一部分只有一个元素,则返回。
- 时间复杂度:O(nlogn)
- 空间复杂度:O(nlogn)
- 稳定性:不稳定
package Sorted;
import java.util.Arrays;
public class QuickSort {
public static void quickSort(int[] nums, int l, int r) {
if(r - l <= 1) return;
int index =(int) (l + Math.random() * (r - l)); //随机选取基数
index = nums[index];
int[] se = partition(nums, l, r, index); //获取等于基数的区域开始位置和结束位置
quickSort(nums, l, se[0]);
quickSort(nums, se[1] + 1, r);
}
public static int[] partition(int[] nums, int l, int r, int index) {
int less = l; //小于基数区指针
int more = r-1; //大于基数区指针
for(int i=l; i<=more; ++i) {
while(i <= more && nums[i] > index) { //大于基数的交换至右边,同时保证交换过来的值小于等于基数
int temp = nums[i];
nums[i] = nums[more];
nums[more] = temp;
--more;
}
if(nums[i] < index) { //小于基数的放在左边
int temp = nums[i];
nums[i] = nums[less];
nums[less] = temp;
++less;
}
}
return new int[] {less, more}; //中间的都是等于基数的
}
public static void main(String[] args) {
int[] nums = new int[30];
for(int i=0;i<nums.length;++i) {
nums[i] = (int)(Math.random() * 100); //随机产生30个数字
}
int[] nums2 = Arrays.copyOfRange(nums, 0, nums.length);
System.out.println(Arrays.toString(nums));
quickSort(nums, 0, nums.length);
System.out.println(Arrays.toString(nums));
Arrays.sort(nums2);
System.out.println(Arrays.toString(nums2));
}
}
七、堆排序
- 基本思想: 利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
- 时间复杂度:O(nlogn)
- 空间复杂度:O(1)
- 稳定性:不稳定
package Sorted;
import java.util.Arrays;
public class HeapSort {
private int[] heap;
private int heap_size;
private int max;
public HeapSort() {
this.heap = new int[400];
this.heap_size = 0;
this.max = 400;
}
private void increaseSize() { //成倍扩容
int[] new_heap = new int[this.max * 2];
this.max *= 2;
for(int i=0; i<this.heap_size; ++i) {
new_heap[i] = this.heap[i];
}
this.heap = new_heap;
}
public void insertElement(int e) {
if(this.heap_size >= this.max) {
this.increaseSize();
}
this.heap[this.heap_size++] = e;
int cur = this.heap_size - 1;
while(this.heap[cur] < this.heap[(cur - 1) / 2]) { //向上整理
int p = (cur - 1) / 2;
int temp = this.heap[p];
this.heap[p] = this.heap[cur];
this.heap[cur] = temp;
cur = p;
}
}
public boolean isEmpty() {
return this.heap_size == 0;
}
public Integer pollElement() {
if(this.isEmpty()) {
return null;
}
int ret = this.heap[0];
this.heap[0] = this.heap[--(this.heap_size)];
int cur = 0;
while((cur * 2 + 1) < this.heap_size) { //有孩子
int min = (cur * 2 + 2) > this.heap_size? cur * 2 + 1: this.heap[cur * 2 + 1] < this.heap[cur * 2 + 2]? cur * 2 + 1: cur * 2 + 2;
if(this.heap[min] >= this.heap[cur]) { // 孩子都比父亲大跳出循环整理完成
break;
}
int temp = this.heap[min];
this.heap[min] = this.heap[cur];
this.heap[cur] = temp;
cur = min;
}
return ret;
}
public static void heapSort(int[] nums) {
int len = nums.length;
HeapSort hs = new HeapSort();
for(int i=0; i<len; ++i) {
hs.insertElement(nums[i]);
}
for(int i=0; i<len; ++i) {
nums[i] = hs.pollElement();
}
}
public static void main(String[] args) {
int[] nums = new int[30];
for(int i=0;i<nums.length;++i) {
nums[i] = (int)(Math.random() * 100); //随机产生30个数字
}
int[] nums2 = Arrays.copyOfRange(nums, 0, nums.length);
System.out.println(Arrays.toString(nums));
heapSort(nums);
System.out.println(Arrays.toString(nums));
Arrays.sort(nums2);
System.out.println(Arrays.toString(nums2));
}
}
八、计数排序
- 基本思想: 是一种稳定的排序算法。计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。它只能对整数进行排序。
- 时间复杂度:O(n+k)
- 空间复杂度:O(k)
- 稳定性:稳定
package Sorted;
import java.util.Arrays;
public class CountingSort {
public static void countingSort(int[] nums, int k) { //k为数字的最大值
int[] cnt = new int[k];
int len = nums.length;
for(int i = 0; i<len; ++i) { // 计数
++cnt[nums[i]];
}
int l = 0;
for(int i=0; i<k; ++i) { //回填
while(cnt[i] > 0) {
nums[l++] = i;
-- cnt[i];
}
}
}
public static void main(String[] args) {
int[] nums = new int[30];
for(int i=0;i<nums.length;++i) {
nums[i] = (int)(Math.random() * 100); //随机产生30个数字
}
int[] nums2 = Arrays.copyOfRange(nums, 0, nums.length);
System.out.println(Arrays.toString(nums));
countingSort(nums, 100);
System.out.println(Arrays.toString(nums));
Arrays.sort(nums2);
System.out.println(Arrays.toString(nums2));
}
}
九、桶排序
- 基本思想:桶排序 是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序。
- 时间复杂度:O(n+k)
- 空间复杂度:O(n+k)
- 稳定性:稳定
package Sorted;
import java.util.ArrayList;
import java.util.Arrays;
public class BucketSort {
public static void buckeSort(Integer[] temp2, int bucketSize) {
if(temp2.length <= 1) return;
int max = temp2[0];
int min = temp2[0];
for(int i=1; i<temp2.length; ++i) {
if(max < temp2[i]) max = temp2[i];
if(min > temp2[i]) min = temp2[i];
}
int bucketCount = ((max - min) / bucketSize) + 1; //计算桶的数量
ArrayList<Integer>[] bucket = new ArrayList[bucketCount];
for(int i=0; i<bucket.length; ++i) { //创建桶
bucket[i] = new ArrayList<>();
}
for(int i=0; i<temp2.length; ++i) { //入桶
bucket[(temp2[i] - min) / bucketSize].add(temp2[i]);
}
int k = 0;
for(int i=0; i<bucket.length; ++i) { //出桶
if(bucketSize == 1) { //有重复数组
for(int j=0; j<bucket[i].size(); ++j) {
temp2[k++] = bucket[i].get(j);
}
}else {
if(bucketCount == 1) //桶的大小太大
--bucketSize;
Integer[] temp = bucket[i].toArray(new Integer[bucket[i].size()]); //转化为数组
buckeSort(temp, bucketSize);
for(int j=0; j<temp.length; ++j) {
temp2[k++] = temp[j];
}
}
}
}
public static void main(String[] args) {
Integer[] nums = new Integer[30];
int[] nums2 = new int[nums.length];
for(int i=0;i<nums.length;++i) {
nums[i] = (int)(Math.random() * 100); //随机产生30个数字
nums2[i] = nums[i];
}
System.out.println(Arrays.toString(nums));
buckeSort(nums, 100);
System.out.println(Arrays.toString(nums));
Arrays.sort(nums2);
System.out.println(Arrays.toString(nums2));
}
}
十、基数排序
- 基本思想:基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。
- 时间复杂度:O(n*k)
- 空间复杂度:O(n+k)
- 稳定性:稳定
package Sorted;
import java.util.Arrays;
public class RadixSort {
public static void radixSort(int[] nums, int digit) {
int[] help = new int[nums.length]; //辅助数组
int bit = maxBit(nums, digit);
for(int i=0; i<bit; ++i) {
int[] count = new int[digit]; //桶用来统计数字应该出现的位置
for(int j=0; j<nums.length; ++j) { //先统计数字的个数
int num = getBit(nums[j], digit, i);
++count[num];
}
for(int j=1; j<digit; ++j) { //换算成位数
count[j] += count[j-1];
}
for(int j=nums.length-1;j>=0;--j) { //从后遍历,保证较大的数,排在较后的位置
int num = getBit(nums[j], digit, i);
help[count[num] - 1] = nums[j];
--count[num];
}
for(int j=0;j<nums.length;++j) { //回填
nums[j] = help[j];
}
}
}
public static int getBit(int num, int digit, int loop) { //获取第几位上的数字
int ans = 0;
for(int i=0;i<=loop;i++) {
ans = num % digit;
num /= digit;
}
return ans;
}
public static int maxBit(int[] nums, int digit) { //获取最大位数
int ans = 0;
int max = nums[0];
for(int i: nums) {
if(max < i) {
max = i;
}
}
do {
max = max/digit;
ans += 1;
}while(max >0);
return ans;
}
public static void main(String[] args) {
int[] nums = new int[10];
for(int i=0;i<nums.length;++i) {
nums[i] = (int)(Math.random() * 100); //随机产生30个数字
}
int[] nums2 = Arrays.copyOfRange(nums, 0, nums.length);
System.out.println(Arrays.toString(nums));
radixSort(nums, 10);
System.out.println(Arrays.toString(nums));
Arrays.sort(nums2);
System.out.println(Arrays.toString(nums2));
}
}
总结
以上就是十种排序算法,其各自具体的时间复杂度,空间复杂度,以及稳定性总结如下:
截止目前基于比较的排序算法还未发现时间复杂度可以突破O(nlogn)的算法,也有部分人致力于去证明基于比较的排序时间复杂度最小为O(nlogn)。
关于几种时间复杂度可以达到O(nlogn)的算法提示:(归并排序、快速排序、堆排序)
1.根据一般的经验以及在工程实践中的表现我们一般认为快速排序的算法性能相较于其他两种性能表现上更优,所以对时间有要求一般选用快速排序。
2.如果对稳定性有要求,一般选用归并排序。java中Arrays.sort()对于基本类型其采用的是快速排序,而非基本类型选用的就是归并排序。
3.对于空间有要求的话,我们一般选用堆排序。
补充小数和问题(归并排序思想)
问题描述
在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组 的小和。
例子:
[1,3,4,2,5] 1左边比1小的数,没有; 3左边比3小的数,1; 4左边比4小的数,1、3; 2左边比2小的数,1; 5左边比5小的数,1、3、4、2; 所以小和为1+1+3+1+1+3+4+2=16。
解法
package AlgorithmProblem;
import java.util.Arrays;
public class SmallNumSum {
public static int solution(int[] nums, int l, int r) {
if(r-l <= 1) {
return 0; //只有一个数没有小数和
}
int sum = 0;
int mid = l + ((r - l) >> 1);
int left = solution(nums, l, mid); //左边的小数和
int right = solution(nums, mid, r); //右边的小数
int merge_sum = merge(nums, l, mid, r); //合并之后产生的小数和
sum = left + right + merge_sum;
return sum;
}
public static int merge(int[] nums, int l, int mid, int r) {
int sum = 0;
int lp = l;
int rp = mid;
int[] help = new int[r - l];
int k = 0;
while(lp < mid && rp < r) {
if(nums[rp] > nums[lp]) { //右边的数字比左边的大则sum加上左边的数字,但是左边的数字是排好序的,则nums[rp]之后的数字也一定大于nums[lp]所以要乘(r - rp)
sum += nums[lp] * (r - rp);
help[k++] = nums[lp++];
}else {
help[k++] = nums[rp++];
}
}
while(lp < mid) {
help[k++] = nums[lp++];
}
while(rp < r) {
help[k++] = nums[rp++];
}
k = 0;
while(k < help.length) {
nums[l++] = help[k++];
}
return sum;
}
public static void main(String[] args) {
int[] nums = new int[] {4, 1, 5, 2, 7};
int ans = solution(nums, 0, nums.length);
System.out.println(Arrays.toString(nums));
System.out.println(ans);
}
}