排序
经典排序算法的实现
1.时间复杂度为O(n^2)的排序:
(1)冒泡排序:从左至右两两比较,前者比后者大则交换顺序,一直到最后一位…(即最先排好最大的数)
例1:
对于一个int数组,请编写一个冒泡排序算法,对数组元素排序。
给定一个int数组A及数组的大小n,请返回排序后的数组。
测试样例:
[1,2,3,5,2,3],6
[1,2,2,3,3,5]
public class BubbleSort {
public int[] bubbleSort(int[] A, int n) {
while(n>0){
for(int i=0;i<n-1;i++){
if(A[i]>A[i+1]){
int temp = A[i];
A[i] = A[i+1];
A[i+1] = temp;
}
}
n--;
}
return A;
}
}
(2)选择排序:选择最小值放在第一位,在剩下的部分选择最小值放到第二位…(即最先排好最小的数)
例:选择排序算法解决例1
public class SelectionSort {
public int[] selectionSort(int[] A, int n) {
for(int i=0;i<n;i++){
for(int j=i+1;j<n;j++){
if(A[i]>A[j]){
int temp = A[i];
A[i] = A[j];
A[j] = temp;
}
}
}
return A;
}
}
(3)插入排序:0位和1位比较,若大于则交换位置,否则不交换;2位再和0位、1位比较,选择位置插入…
例:插入排序算法解决例1
public class InsertionSort {
public int[] insertionSort(int[] A, int n) {
for(int i=1;i<n;i++){
for(int j=i;j>0;j--){
if(A[j]<A[j-1]){
int temp = A[j];
A[j] = A[j-1];
A[j-1] = temp;
}
}
}
return A;
}
}
2.时间复杂度为O(n*log2n)的排序:
(1)归并排序:将相邻数作为一个子序列进行排序,再将相邻子序列作为整体序列进行排序…
例:归并排序算法解决例1
public class MergeSort {
public int[] mergeSort(int[] A, int n) {
sort(A,0,n-1);
return A;
}
//分组
public void sort(int[] A,int a,int b){
if(a<b){
//递归调用,先分组再合并排序
int mid = (a+b)/2;
sort(A,a,mid);
sort(A,mid+1,b);
merge(A,a,mid,b);
}
}
//合并两个分组
public void merge(int[] arr,int a,int mid,int b){
int i=a;
int j=mid+1;
int k=i;
int[] temp = new int[arr.length];
while(i<=mid&&j<=b){
if(arr[i]<arr[j]){
temp[k++]=arr[i++];
}else{
temp[k++]=arr[j++];
}
}
//剩余未合并的部分
while(i<=mid){
temp[k++]=arr[i++];
}
while(j<=b){
temp[k++]=arr[j++];
}
//将临时数组放回原数组相应位置
while(a<=b){
arr[a]=temp[a++];
}
}
}
(2)快速排序:随机选一个数,小于这个数的放在左边,大于的放在右边,分为两部分,每个部分再次选择…
例:快速排序算法解决例1
public class QuickSort {
public int[] quickSort(int[] A, int n) {
quickSort(A,0,n-1);
return A;
}
//分组
public void quickSort(int[] A,int start,int end){
if(start<end){
//递归调用,先交换数字,再分组
int index = sort(A,start,end);
if(start<index-1)
quickSort(A,start,index-1);
if(index<end)
quickSort(A,index,end);
}
}
//对选择的数字两边进行交换,即左边的小于此数,右边的大于此数
public int sort(int[] A,int start,int end){
int val = A[(start+end)/2];
while(start<=end){
while(A[start]<val)
start++;
while(val<A[end])
end--;
if(start<=end){
int tmp=A[end];
A[end]=A[start];
A[start]=tmp;
start++;
end--;
}
}
return start;//返回选择的数字的坐标
}
}
(3)堆排序:构建大根堆,将根节点和最后一个节点交换并脱出最后一个节点,放入有序序列(最大),再次调整堆为大根堆…
大根堆:每个结点的关键字都不小于其孩子结点的关键字
构建大根堆:用无序序列建立完全二叉树,再从根节点开始检验
例:堆排序算法解决例1
public class HeapSort {
public int[] heapSort(int[] A, int n) {
if (A == null || n == 1) {
return A;
}
creatMaxHeap(A,n);
for (int i = n - 1; i >= 1; i--) {
// 把第一个元素和最后一个元素交换位置
swap(A, 0, i);
// 继续调整下面的
maxHeap(A, i, 0);
}
return A;
}
//构建初始大根堆
public void creatMaxHeap(int[] A,int n){
int index = n / 2;//完全二叉树有孩子的节点个数为n/2
for (int i = index-1; i >= 0; i--) {//如果从上往下调整,下面大的数则会换不上来
maxHeap(A,n,i);
}
}
//堆调整
public void maxHeap(int[] A,int n,int index){
int left = index*2 + 1;
int right = index*2 + 2;
int maxindex = index;
if(left<n && A[left]>A[maxindex])
maxindex = left;
if(right<n && A[right]>A[maxindex])
maxindex = right;
if(maxindex != index){//如果不等则说明根的值不是最大的
swap(A,index,maxindex);
maxHeap(A,n,maxindex);//递归,换下去的小值与其左右孩子比较调整
}
}
//调换位置
private void swap(int[] A,int a,int b){
int temp = A[a];
A[a] = A[b];
A[b] = temp;
}
}
(4)希尔排序:
步长为3,从无序序列第4位开始,与它往前三3位进行比较,直到步长内无数字可比较…
完成第一次排序后,步长减一再次进行排序,直到步长为1,结束排序
例:希尔排序算法解决例1
public class ShellSort {
public int[] shellSort(int[] A, int n) {
if(A==null || n<2)
return A;
int feet = n/2;
int index = 0;
while(feet>0){
for(int i=feet;i<n;i++){
index = i;
while(index>=feet){
if(A[index]<A[index-feet]){
int temp = A[index];
A[index] = A[index-feet];
A[index-feet] = temp;
index = index - feet;
}else{
break;
}
}
}
feet = feet/2;
}
return A;
}
}
3.时间复杂度为O(n)的排序(非比较排序)
(1)计数排序:一组数在排序之前先统计这组数中其他数小于这个数的个数,则可以确定这个数的位置
例:计数排序算法解决例1
public class CountingSort {
public int[] countingSort(int[] A, int n) {
if(A==null || n==0)
return null;
//取得最大最小值
int max = A[0];
int min = A[0];
for(int i=0;i<n;i++){
if(max < A[i])
max = A[i];
if(min > A[i])
min = A[i];
}
int temp[] = new int[max-min+1];//建立辅助数组
for(int i=0;i<n;i++){
temp[A[i]-min]++;//找出每个数字出现的次数,并放入数组中相应的位置
}
int index = 0; //表示下标
for(int i=0;i<max-min+1;i++){
while(temp[i] > 0){//循环次数为每个桶中的数字,即每个数字出现的次数
A[index++] = i + min;
//i+min为排序数组中相应的数字,index++表示小于这个数的个数(从左往右)
temp[i]--;
}
}
return A;
}
}
(2)基数排序(基于桶排序)
桶排序思想:
将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)
public class RadixSort {
public int[] radixSort(int[] A, int n) {
int m=1;//代表位数对应的数:1,10,100...
int k=0;//保存每一位排序后的结果用于下一位的排序输入
int[][] bucket=new int[10][n];//排序桶用于保存每次排序后的结果,这一位上排序结果相同的数字放在同一个桶里
int[] order=new int[n];//用于保存每个桶里有多少个数字
while(m<1000)
{
for(int num:A) //将数组array里的每个数字放在相应的桶里
{
int digit=(num/m)%10;
bucket[digit][order[digit]]=num;
order[digit]++;
}
for(int i=0;i<n;i++)//将前一个循环生成的桶里的数据覆盖到原数组中用于保存这一位的排序结果
{
if(order[i]!=0)//这个桶里有数据,从上到下遍历这个桶并将数据保存到原数组中
{
for(int j=0;j<order[i];j++)
{
A[k]=bucket[i][j];
k++;
}
}
order[i]=0;//将桶里计数器置0,用于下一次位排序
}
m*=10;
k=0;//将k置0,用于下一轮保存位排序结果
}
return A;
}
}
4.排序总结:
(1)算法特点:
双重循环:冒泡排序、插入排序、选择排序
三重循环:希尔排序
递归方法:归并排序(先分组再调整)、快速排序(先调整再分组)、堆排序(大根堆、堆调整)
(2)排序关系:
类别 | 排序方法 | 时间复杂度(平均) | 空间复杂度 | 稳定性 |
---|---|---|---|---|
交换排序 | 冒泡排序 | O(N2) | O(1) | 稳定 |
↑ | 快速排序 | O(N*logN) | O(log2n)~O(n) | 不稳定 |
选择排序 | 直接选择 | O(N2) | O(1) | 不稳定 |
↑ | 堆排序 | O(N*logN) | O(1) | 不稳定 |
插入排序 | 直接插入 | O(N2) | O(1) | 稳定 |
↑ | 希尔排序 | O(N1.3) | O(1) | 不稳定 |
归并排序 | ~ | O(N*logN) | O(n) | 稳定 |
桶排序 | 计数排序 | O(N) | O(M) | 稳定 |
↑ | 基数排序 | O(N) | O(M) | 稳定 |
注:桶排序的M为桶的个数
稳定性,即待排序记录中相同的关键字记录,若经过排序,其相对次序不变,则称改排序方法是稳定的
经典排序算法的应用
1.已知一个几乎有序的数组,几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离可以不超过k,并且k相对于数组来说比较小。请选择一个合适的排序算法针对这个数据进行排序。
给定一个int数组A,同时给定A的大小n和题意中的k,请返回排序后的数组。
测试样例:
[2,1,4,3,6,5,8,7,10,9],10,2
返回:[1,2,3,4,5,6,7,8,9,10]
解决:改进后的堆排序,即使用小根堆,非递归堆排序,时间复杂度为N*logK
思路:0~k元素创建小根堆,堆顶写入A,并以k后的元素为堆顶,调整堆,循环至A最后一个元素;
最后再将n-k到k的剩下元素写入A
public class ScaleSort {
public int[] sortElement(int[] A, int n, int k) {
if(A==null||A.length==0||n<k)
return null;
int[] temp = creatMinHeap(A,k);
for(int i=k;i<n;i++){
A[i-k] = temp[0];//将堆顶最小值放到A[0]
temp[0] = A[i];//将堆顶变为下标k之后的值
adjustHeap(temp,0,k);//调整temp小根堆
}
for (int i = n-k; i < n; i++){//下标k之后的值已经没有了,则将剩下值生成的小根堆依此添加回A中(从n-k开始)
A[i] = temp[0];
swap(temp,0, k-1);//将堆顶和最后一个交换
adjustHeap(temp,0,--k);//脱出最后一个已放入元素,并重新调整(小根堆的顺序不一定是有序的)
}
return A;
}
//0~k建立初始化小根堆,两种方法:也可以直接使用adjustHeap方法生成详见堆排序
private int[] creatMinHeap(int[] A,int k){
int[] temp = new int[k];
for(int i=0;i<k;i++){
minHeap(temp,A[i],i);
}
return temp;
}
private void minHeap(int[] temp,int value,int index){
temp[index] = value;
while(index!=0){
int parent = (index-1)/2;
if(temp[parent]>temp[index]){
swap(temp,parent,index);
index = parent;
}else{
break;
}
}
}
//调整小根堆(非递归写法,可运用于大根堆)
private void adjustHeap(int[] temp,int index,int k){
int left = index*2+1;
int right = index*2+2;
int minindex = index;
while(left<k){
if(temp[left]<temp[minindex])
minindex = left;
if(right<k && temp[right]<temp[minindex])
minindex = right;
if(minindex!=index){
swap(temp,index,minindex);
}else{
break;
}
index = minindex;
left = index * 2 + 1;
right = index * 2 + 2;
}
}
public void swap(int[] arr, int index1, int index2) {
int tmp = arr[index1];
arr[index1] = arr[index2];
arr[index2] = tmp;
}
}
2.请设计一个高效算法,判断数组中是否有重复值。必须保证空间复杂度为O(1)【无这个条件则使用哈希表】
给定一个int数组A及它的大小n,请返回它是否有重复值。
测试样例:
[1,2,3,4,5,5,6],7
返回:true
解决:非递归堆排序(递归堆排序空间复杂度为O(logN))
思路:结合经典排序算法实现和应用中的堆排序(大根堆、非递归)
public class Checker {
public boolean checkDuplicate(int[] a, int n) {
if(a==null||n==0)
return false;
heapSort(a,n);
for(int i=0;i<n-1;i++){
if(a[i]==a[i+1])
return true;
}
return false;
}
private void heapSort(int[] a,int n){
for (int i = n/2-1; i>=0;i--){
maxHeap(a,n,i);
}
for (int i=n-1;i>=1;i--) {
swap(a, 0, i);
maxHeap(a, i, 0);
}
}
public void maxHeap(int[] A,int n,int index){
int left = index*2 + 1;
int right = index*2 + 2;
int maxindex = index;
while(left<n){
if(A[left]>A[maxindex])
maxindex = left;
if(right<n && A[right]>A[maxindex])
maxindex = right;
if(maxindex != index){
swap(A,index,maxindex);
index = maxindex;
left = index*2 + 1;
right = index*2 + 2;
}else{
break;
}
}
}
private void swap(int[] A,int a,int b){
int temp = A[a];
A[a] = A[b];
A[b] = temp;
}
}
3.有两个从小到大排序以后的数组A和B,其中A的末端有足够的缓冲空容纳B。请编写一个方法,将B合并入A并排序。
给定两个有序int数组A和B,A中的缓冲空用0填充,同时给定A和B的真实大小int n和int m,请返回合并后的数组。
思路:两数组从后开始比较,大的放入A的最后的空位
public class Merge {
public int[] mergeAB(int[] A, int[] B, int n, int m) {
int i = n-1;
int j = m-1;
int k = m+n-1;
while(i>=0&&j>=0){
if(A[i]>B[j]){
A[k] = A[i];
i--;
}else{
A[k] = B[j];
j--;
}
k--;
}
while(j>=0){//可能A先排完,而B还没有全部放入A
A[k]=B[j];
j--;
k--;
}
return A;
}
}
4.有一个只由0,1,2三种元素构成的整数数组,请使用交换、原地排序而不是使用计数进行排序。
给定一个只含0,1,2的整数数组A及它的大小,请返回排序后的数组。保证数组大小小于等于500。
测试样例:
[0,1,1,0,2,2],6
返回:[0,0,1,1,2,2]
思路:定义三个坐标,依此判断数组,为0或2则与头(first++)尾(end–)元素的交换,为1则index++
public class ThreeColor {
public int[] sortThreeColor(int[] A, int n) {
int index = 0;
int first = 0;
int end = n-1;
while(index<=end){
if(A[index]==1){
index++;
}else if(A[index]==0){
int temp = A[first];
A[first] = A[index];
A[index] = temp;
index++;
first++;
}else if(A[index]==2){
int temp = A[end];
A[end] = A[index];
A[index] = temp;
end--;
}
}
return A;
}
}
5.现在有一个行和列都排好序的矩阵,请设计一个高效算法,快速查找矩阵中是否含有值x。
给定一个int矩阵mat,同时给定矩阵大小nxm及待查找的数x,请返回一个bool值,代表矩阵中是否存在x。所有矩阵中数字及x均为int范围内整数。保证n和m均小于等于1000。
测试样例:
[[1,2,3],[4,5,6],[7,8,9]],3,3,10
返回:false
思路:从矩阵最上角的元素开始比较,小则下移,大则右移,直到找到相应的数或不存在此数
public class Finder {
public boolean findX(int[][] mat, int n, int m, int x) {
int r = 0;//行
int c = m-1;//列
while(r<n&&c>=0){
if(mat[r][c]==x){
return true;
}else if(mat[r][c]>x){
c--;
}else if(mat[r][c]<x){
r++;
}
}
return false;
}
}
6.对于一个数组,请设计一个高效算法计算需要排序的最短子数组的长度。
给定一个int数组A和数组的大小n,请返回一个二元组,代表所求序列的长度。(原序列位置从0开始标号,若原序列有序,返回0)。保证A中元素均为正整数。
测试样例:
[1,4,6,5,9,10],6
返回:2
思路:从左到右记录最大的数,记录没有被记录的最右边;从右到左记录最小的数,记录没有被记录的最左边,两者之差+1即为需要排序的最短子数组的长度
public class Subsequence {
public int shortestSubsequence(int[] A, int n) {
if(A==null||n<=1)
return 0;
int right = 0;
int left = n-1;
int max = A[0],min = A[n-1];
for(int i=0;i<n;i++){
if(A[i]>=max)
max = A[i];
else
right = i;
}
for(int j=n-1;j>=0;j--){
if(A[j]<=min)
min = A[j];
else
left = j;
}
return right<left?0:right-left+1;
}
}
7.有一个整形数组A,请设计一个复杂度为O(n)的算法,算出排序后相邻两数的最大差值。
给定一个int数组A和A的大小n,请返回最大的差值。保证数组元素多于1个。
测试样例:
[1,2,5,4,6],5
返回:2
思路:
1.找出数组的最大值 max 和最小值 min
2. [min,max) 等量地分成 n 个区间;每个数根据各自的区间选择进入的桶,并把最大值单独放在n+1号桶中。
3.桶数为 n+1,元素只有 n 个,因此必然存在空桶。则最大的差值必然在空桶的两侧,即后一个桶的最小值和前一个桶的最大值。
public class Gap {
public int maxGap(int[] A, int n) {
if(A==null||n==0)
return 0;
//表示桶内是否含有元素
boolean[] hasNum = new boolean[n + 1];
//表示桶内元素中的最小值
int[] mins = new int[n + 1];
//表示桶内元素中的最大值
int[] maxs = new int[n + 1];
//定义最大值,最小值,桶编号
int min=0,max=0,bid=0;
for(int temp : A){
min = Math.min(min,temp);
max = Math.max(max,temp);
}
if(min==max)
return 0;
for(int i=0;i<n;i++){
bid = ((A[i] - min) * n / (max - min));//计算桶编号,(max-min)/n为桶范围
mins[bid] = hasNum[bid] ? Math.min(A[i],mins[bid]) : A[i];
//判断桶内有无元素,有则比较保留小的;没有则直接保留元素
maxs[bid] = hasNum[bid] ? Math.max(A[i],maxs[bid]) : A[i];//同理
hasNum[bid] = true;
}
int res = 0;
int maxtemp = maxs[0];
for(int i=1;i<=n;i++){
if(hasNum[i]){
res = Math.max(res,mins[i]-maxtemp);//后一个桶的最小值减去前一个桶的最大值
maxtemp = maxs[i];//当桶内无元素时,max的下标不会增加
}
}
return res;
}
}