1. 时间复杂度为O(N^2)的排序算法
冒泡,选择,插入排序
- 冒泡排序:时间复杂度O(n^2) 与数组元素原始顺序无关
import java.util.*;
public class BubbleSort {
public int[] bubbleSort(int[] A, int n) {
// 一共需要循环n-1次
for(int i=1,index=n;i<=n-1;i++,index--){
//从索引1到索引index,依次比较其与前一个数的大小,大的调换在后面,直到第index个数
//初始时最后一个比较的数为索引n-1,每次大循环时递减
for(int j=1;j<index;j++){
if(A[j]<A[j-1]){
int temp=A[j];
A[j]=A[j-1];
A[j-1]=temp;
}
}
}
return A;
}
}
- 选择排序:时间复杂度O(n^2) 与数组元素原始顺序无关
import java.util.*;
public class SelectionSort {
public int[] selectionSort(int[] A, int n) {
//一共循环n-1次
//每次假定比较序列中第一个数index-1为当前最小值,依次将最小值索引上的数与序列中后面的数进行比较,如果有更小的则更新最小值索引
//内部循环结束则调换当前比较序列的第一个数和最小值索引的数
for(int i=1,index=1;i<=n-1;i++,index++){
int minIndex=index-1;
for(int j=index;j<=n-1;j++){
if(A[j]<A[minIndex]){
minIndex=j;
}
}
int temp=A[index-1];
A[index-1]=A[minIndex];
A[minIndex]=temp;
}
return A;
}
}
- 插入排序:时间复杂度O(n2) 跟原始元素顺序有关,如果移动距离不超过k,那么时间复杂度为O(N*K)
import java.util.*;
//插入排序
public class InsertionSort {
public int[] insertionSort(int[] A, int n) {
//循环n-1次,从第1个数一直到第n-1个数,
//循环index-1次,依次比较第index个数与前面的数的大小,若index上的数小于等于前面的数,则调换位置,index与index-1到0上的数依次比较
for(int i=1,index=1;i<=n-1;i++,index++){
int tempIndex=index;//当前的数A[index],由于调换过程中会发生位置改变
for(int j=index-1;j>=0;j--){
if(A[tempIndex]<=A[j]){
int temp=A[j];
A[j]=A[tempIndex];
A[tempIndex]=temp;
tempIndex=j;
}
}
}
return A;
}
}
2. 时间复杂度为O(N*logN)的排序算法
- 归并排序:递归地分解为两半并按大小顺序合并为一个有序数组 与数组元素原始顺序无关
import java.util.*;
//归并排序 递归解决 分解和合并
public class MergeSort {
public int[] mergeSort(int[] A, int n) {
if(A==null||n<2){
return A;
}
process(A,0,n-1);
return A;
}
//子序列left到right,先对分为两半,再将两部分按从小到大顺序合并
public void process(int[] A,int left,int right){
//left小于right索引才可以继续分
if(left<right){
int mid=(left+right)/2;
process(A,left,mid);
process(A,mid+1,right);
merge(A,left,mid,right);
}
}
//将两部分根据从小到大的顺序进行排序合并,利用指针分别遍历两部分来比较大小,加入缓存数组
public void merge(int[] A,int left,int mid,int right){
int leftIndex=left;
int rightIndex=mid+1;
int tempIndex=0;
int[] temp=new int[right-left+1];//保存排序后结果
//分别遍历两部分,比较大小并存入缓存数组
while(leftIndex<=mid&&rightIndex<=right){
if(A[leftIndex]<=A[rightIndex]){
temp[tempIndex++]=A[leftIndex++];
}else{
temp[tempIndex++]=A[rightIndex++];
}
}
//将两部分中其中一半的剩余部分加入
while(leftIndex<=mid){
temp[tempIndex++]=A[leftIndex++];
}
while(rightIndex<=right){
temp[tempIndex++]=A[rightIndex++];
}
//用排序合并后的数组替换原来的数组
tempIndex=0;
while((left+tempIndex)<=right){
A[left+tempIndex]=temp[tempIndex++];
}
}
}
- 快速排序:划分过程时间复杂度为O(n) 与数组元素原始顺序无关
import java.util.*;
//快速排序
public class QuickSort {
public int[] quickSort(int[] A, int n) {
if(A==null||n<2){
return A;
}
process(A,0,n-1);
return A;
}
public void process(int[] A,int left,int right){
if(left<right){
//在[left,right]随机选取一个数并放到最后以便比较
int randomIndex=left+(int)(Math.random()*(right-left+1));
swap(A,randomIndex,right);
//进行划分
int mid=partition(A,left,right);
process(A,left,mid-1);
process(A,mid+1,right);
}
}
//遍历整个数组,与随机数做比较进行一次划分,小于随机数的放到mid索引前面,大于随机数的放到mid索引后面,返回小于等于随机数的个数即mid
public int partition(int[] A,int left,int right){
int pivot=left-1;
int index=left;
//要加上等号,因为还要把random数从最后放回有序序列的最后
while(index<=right){
if(A[index]<=A[right]){
swap(A,index,++pivot);
}
index++;
}
return pivot;
}
//交换元素
public void swap(int[] A,int index1,int index2){
int temp=A[index1];
A[index1]=A[index2];
A[index2]=temp;
}
}
堆排序
堆排序讲解
堆实际上是一棵完全二叉树,其任何一非叶节点满足性质: Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]或者Key[i]>=Key[2i+1]&&key>=key[2i+2](i表示在数组中非叶节点的索引,其左右孩子在数组中的索引分别为2i+1,2i+2,数组存储时按照宽度优先遍历存储这棵完全二叉树的每个节点),即任何一非叶节点的关键字不大于或者不小于其左右孩子节点的关键字。
import java.util.*;
//堆排序 大根堆 任何一非叶节点的关键字不小于其左右孩子节点的值
public class HeapSort {
public int[] heapSort(int[] A, int n) {
buildHeap(A,n);
for(int i=n-1;i>=0;i--){
swap(A,0,i);//交换大根堆的头结点到数组的最后
adjustHeap(A,0,i);//调整以0号元素为头结点的前i个值为大根堆
}
return A;
}
//调换元素位置
public void swap(int[] A,int index1,int index2){
int temp=A[index1];
A[index1]=A[index2];
A[index2]=temp;
}
//建立初始大根堆,只执行一次
//整个序列,length/2到0均为非叶节点,以非叶节点为头的子树需要调整为大根堆 i*2+1为i的左孩子,i*2+2为i的右孩子
//从下到上开始建立
public void buildHeap(int[] A,int length){
for(int i=length/2;i>=0;i--){
adjustHeap(A,i,length);
}
}
//调整以father为头节点的子树,所有父节点的值大于等于其孩子节点的值
public void adjustHeap(int[] A,int father,int length){
//保存最开始父节点的值 father总是指向该值在数组中的索引
int tempMax=A[father];
for(int child=father*2+1;child<length;child=child*2+1){
//child指向左孩子和右孩子中最大值
if(child<(length-1)&&A[child]<A[child+1])
{
child++;
}
//比较父节点和最大的孩子节点的值,如果父节点小于孩子值,则用最大孩子值覆盖父节点的值
if(tempMax>A[child]){
break;
}
A[father]=A[child];
father=child;
}
A[father]=tempMax;
}
}
- 希尔排序:插入排序的变种,步长是递减的,当步长为1时为插入排序,关键是步长选择
import java.util.*;
//希尔排序
public class ShellSort {
public int[] shellSort(int[] A, int n) {
//步长递减,从n/2到1
for(int step=n/2;step>=1;step=step/2){
//遍历一遍以step为间隔的各个元素与它前面每隔step上的元素大小
for(int index=step;index<=n-1;index++){
//tempIndex是index每隔step上的元素索引
for(int tempIndex=index-step;tempIndex>=0;tempIndex-=step){
if(A[index]<A[tempIndex]){
swap(A,index,tempIndex);
index=tempIndex;
}else{
//停止比较,因为前面每隔step
break;
}
}
}
}
return A;
}
public void swap(int[] A,int index1,int index2){
int temp=A[index1];
A[index1]=A[index2];
A[index2]=temp;
}
}
3.时间复杂度为O(N)的排序算法
- 计数排序:根据最小值最大值间隔确定桶的个数,把每个数依次放到对应的桶上,最后遍历桶,即为有序的序列。三个循环。需要知道最大最小值。时间复杂度为O(n)
import java.util.*;
//计数排序
public class CountingSort {
public int[] countingSort(int[] A, int n) {
int min=A[0];
int max=A[0];
//确定桶的个数,即最大值减去最小值
for(int i=1;i<n;i++){
if(A[i]<min){
min=A[i];
continue;
}
if(A[i]>max){
max=A[i];
}
}
//建立桶数组
int bucketSize=max-min+1;
int[] bucket=new int[bucketSize];
for(int i=0;i<n;i++){
bucket[A[i]-min]+=1;
}
int[] result=new int[n];
for(int i=0,index=0;i<bucketSize;i++){
while(bucket[i]>0){
result[index++]=min+i;
bucket[i]--;
}
}
return result;
}
}
- 基数排序:如果是整数,从小的位数到大的位数,每个位数上都根据位数上的值进行依次排序,最后得到一个有序序列。位数决定了需要排序的次数。需要知道排序的元素的取值范围。时间复杂度为O(n)
import java.util.*;
//基数排序
public class RadixSort {
public int[] radixSort(int[] A, int n) {
//位数决定了排序的总次数,由于题目中说明元素小于等于2000,所以需要排4次
int[][] bucket=new int[10][n];
int[] size=new int[10];//记录每一个桶上元素的个数
for(int Bi=0;Bi<4;Bi++){
//对数组元素根据其倒数第Bi上的数的大小放到对应的桶上
for(int Ai=0;Ai<n;Ai++){
//倒数Bi位上的数字
int num=A[Ai]/(int)Math.pow(10,Bi)%10;
bucket[num][size[num]]=A[Ai];
size[num]++;
}
//遍历桶,取出元素组成新的序列,同时清空桶
for(int i=0,Aindex=0;i<10;i++){
for(int Bj=0;Bj<size[i];Bj++){
A[Aindex++]=bucket[i][Bj];
bucket[i][Bj]=0;
}
size[i]=0;
}
}
return A;
}
}
4. 排序算法的空间复杂度
5. 排序算法的稳定性
函数栈的大小是递归的层数
6.排序算法例题
- 重复值判断:
import java.util.*;
//高效指空间复杂度和时间复杂度均小,非递归的堆排序空间复杂度为O(1),时间复杂度为N*log(N)
public class Checker {
public boolean checkDuplicate(int[] A, int n) {
//非有序的部分长度从n缩减到2,建立小根堆,即数组上元素从小到大
//数组上n/2到0索引前的数,即非叶节点上的数,都小于等于其左右孩子上的数
if(A==null||n==1){
return false;
}
//初始调整建立小根堆 从子树的第一个非叶节点到整棵树数的根节点 保证可以遍历所有的子树
for(int father=n/2;father>=0;father--){
heapAdjust(A,father,n);
}
//交换大根堆的头结点到数组的最后 数组长度依次从右边缩减
for(int index=n-1;index>=0;index--){
swap(A,index,0);
heapAdjust(A,0,index);
}
//如果后面一个值与前面一个值相同,则表示有重复的值
for(int i=1;i<n;i++){
if(A[i]==A[i-1]){
return true;
}
}
return false;
}
public void swap(int[] A,int index1,int index2){
int temp=A[index1];
A[index1]=A[index2];
A[index2]=temp;
}
public void heapAdjust(int[] A,int father,int n){
//以father为头结点的子树为小根堆
int tempMax=A[father];
for(int child=2*father+1;child<n;child=child*2+1){
if(child<(n-1)&&A[child]<A[child+1]){
child++;
}
if(tempMax>=A[child]){
break;
}
A[father]=A[child];
father=child;
}
A[father]=tempMax;
}
}
- 有序数组合并:
有两个从小到大排序以后的数组A和B,其中A的末端有足够的缓冲空容纳B。请编写一个方法,将B合并入A并排序。
给定两个有序int数组A和B,A中的缓冲空用0填充,同时给定A和B的真实大小int n和int m,请返回合并后的数组。
import java.util.*;
//合并有序数组
public class Merge {
public int[] mergeAB(int[] A, int[] B, int n, int m) {
//为了使得有序数组元素移动最少,要从后往前对A数组遍历,同时比较A当前元素与B元素的大小,大的调换到后面
//B遍历完即完成
if(A==null||B==null){
return A!=null?A:B;
}
int Ai=n-1,Bi=m-1,index=n+m-1;
while(Bi>=0&&Ai>=0){
if(A[Ai]>=B[Bi]){
A[index--]=A[Ai--];
}else{
A[index--]=B[Bi--];
}
}
while(Bi>=0){
A[index--]=B[Bi--];
}
return A;
}
}
- 荷兰国旗问题:
有一个只由0,1,2三种元素构成的整数数组,请使用交换、原地排序而不是使用计数进行排序。
给定一个只含0,1,2的整数数组A及它的大小,请返回排序后的数组。保证数组大小小于等于500。
测试样例:[0,1,1,0,2,2],6
返回:[0,0,1,1,2,2]
import java.util.*;
//荷兰国旗问题 三值排序
public class ThreeColor {
public int[] sortThreeColor(int[] A, int n) {
//0值区域和2值区域的索引值
int index0=-1,index2=n,index=0;
//当前索引遍历到2值区域时停止,否则遇到0值则交换其到0值区域,遇到2则交换到2值区域
while(index!=index2){
if(A[index]==2){
swap(A,index,--index2);
//注意交换过来的--index2上的值没有被遍历过,因此不增加index的值
continue;
}else if(A[index]==0){
swap(A,index,++index0);
}
index++;
}
return A;
}
public void swap(int[] A,int index1,int index2){
int temp=A[index1];
A[index1]=A[index2];
A[index2]=temp;
}
}
- 在行和列方向上均有序的矩阵中寻找是否包含某个数:
现在有一个行和列都排好序的矩阵,请设计一个高效算法,快速查找矩阵中是否含有值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
import java.util.*;
//在一个行列有序的矩阵中,寻找是否包含某个数
public class Finder {
public boolean findX(int[][] mat, int n, int m, int x) {
//初始位置可以从左下角或者右上角开始,因为这两个角上沿X轴,Y轴上的数分别是增大和减小的,有利于确定下一个搜索方向是按行还是按列
for(int row=0,column=m-1;row<=n-1&&column>=0;){
if(x==mat[row][column]){
return true;
}
else if(x<mat[row][column]){
column--;
}else{
row++;
}
}
return false;
}
}
- 寻找无序的最短子序列的长度:左右两边分别确定,同选择排序的变种
对于一个数组,请设计一个高效算法计算需要排序的最短子数组的长度。
给定一个int数组A和数组的大小n,请返回一个二元组,代表所求序列的长度。(原序列位置从0开始标号,若原序列有序,返回0)。保证A中元素均为正整数。
测试样例:[1,4,6,5,9,10],6
返回:2
import java.util.*;
//求需要排序的最短子数组
public class Subsequence {
public int shortestSubsequence(int[] A, int n) {
if(A==null||n<2){
return 0;
}
int left=0,right=n-1;
int max,min;
boolean hasLeft=false,hasRight=false;//是否确立了无序子序列左右边界
while((!hasLeft||!hasRight)&&left<right){
//从left+1遍历到n-1,寻找最小值,若当前left的值小于最小值,则继续向右,否则开始从右边向左遍历
if(!hasLeft){
min=A[left+1];
for(int j=left+2;j<n;j++){
if(A[j]<min){
min=A[j];
}
}
if(A[left]>min){
hasLeft=true;
}else{
left++;
}
}
//从right-1遍历到0,寻找最大值,若当前right的值大于最大值,则继续向左,否则跳出循环
if(!hasRight){
max=A[right-1];
for(int k=right-2;k>=0;k--){
if(A[k]>max){
max=A[k];
}
}
if(A[right]<max){
hasRight=true;
}else{
right--;
}
}
}
if(hasLeft&&hasRight){
return right-left+1;
}else{
return 0;
}
}
}
- 相邻两数最大差值
有一个整形数组A,请设计一个复杂度为O(n)的算法,算出排序后相邻两数的最大差值。
给定一个int数组A和A的大小n,请返回最大的差值。保证数组元素多于1个。
测试样例:[1,2,5,4,6],5
返回:2
import java.util.*;
//寻找排序后相邻两个数的最大差距,时间复杂度O(n),因为没有要求空间复杂度,使用桶排序中的计数排序
public class Gap {
public int maxGap(int[] A, int n) {
//寻找数组最大最小值
int min=A[0],max=A[0];
for(int i=1;i<n;i++){
if(A[i]<min){
min=A[i];
}else if(A[i]>max){
max=A[i];
}
}
int bNum=max-min+1;
int[] size=new int[bNum];//每个桶上的元素个数
//桶号和数的对应关系为 数-min size每个元素对应min+索引 这个数出现在A中的次数
for(int i=0;i<n;i++){
size[A[i]-min]+=1;
}
int maxGap=0,next=0;
//遍历计数数组,找出间隔最大的两个桶
for(int i=0;i<bNum;){
next=i+1;
while(next<bNum&&size[next]==0){
next++;
}
if((next-i)>maxGap){
maxGap=next-i;
}
i=next;
}
return maxGap;
}
}