今天笔试,排序考的好多,我才整理了两个,笔试完决定全部整理完
上图是我们所有排序算法的分类:
分析算法的术语说明:
- 稳定:如果a原本在b前面,而a=b,排序之后仍然在b的前面;
- 不稳定:如果a原本在b前面,而a=b,排序之后a可能会出现在b的后面
- 内排序:所有排序操作都在内存中完成
- 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;(涉及文件操作以及归并排序思想)
- 时间复杂度:一个算法执行所耗费的时间
- 空间复杂度:运行完一个程序所需内存大小
一、交换排序
1、 冒泡排序
冒泡排序是一种简单的排序算法。
算法描述
- 比较相邻元素,如果第一个比第二个大,就交换他们两个;
- 对每一对相邻元素作相同的工作,从开始第一对到结尾最后一对,这样在最后的元素会是最大的数;
- 针对所有的元素重复以上步骤,直到排序完成;
代码实现
根据算法描述;使用双重for循环
经过刷题总结,时间复杂度是O(n^2)的算法通常来说并不是最优解;
普通实现
public static void bubbleSort(int []arr){
if(arr==null){
return;
}
for(int i=0;i<arr.length;i++){
for(int j=0;j<arr.length-1;j++){
if(arr[j]>arr[j+1]){
swap(arr,j,j+1);
}
}
}
System.out.println(Arrays.toString(arr));
}
private static void swap(int[] arr, int j, int i) {
int temp=arr[j];
arr[j]=arr[i];
arr[i]=temp;
}
优化:
- 改变j的范围(因为每次遍历一遍后都确定了一个位置,所以length-1-i)
- 定义flag标志位,如果它是true说明遍历一遍没有需要调整的,也就是数据是有序的,直接退出
public static void bubbleSort2(int []arr){
if(arr==null){
return;
}
boolean flag;
for(int i=0;i<arr.length;i++){
flag=true;
for(int j=0;j<arr.length-1-i;j++){
if(arr[j]>arr[j+1]){
swap(arr,j,j+1);
flag=false;
}
}
if(flag){
break;
}
}
System.out.println(Arrays.toString(arr));
}
泛型实现
public static <T extends Comparable<T>>void bubbleSort1(T [] arr){
if(arr==null){
return;
}
boolean flag;
for(int i=0;i<arr.length;i++){
flag=true;
for(int j=0;j<arr.length-1-i;j++){
if(arr[j].compareTo(arr[j+1])>0){
swap(arr,j,j+1);
flag=false;
}
}
if(flag){
break;
}
}
}
private static<T> void swap(T[]arr,int j,int i){
T temp=arr[j];
arr[j]=arr[i];
arr[i]=temp;
}
算法分析
- 时间复杂度:
最优时间复杂度:O(n)
最差时间复杂度:O(n^2)
平均时间复杂度:O(n^2) - 空间复杂度:
平均空间复杂度:O(1) - 稳定性:
稳定
2、快速排序
快排主要是通过选择一个关键值作为基准值。
比基准值小的放在左边;
比基准值大的放在右边;
依次递归。
算法描述
- 选择基准
- 分割操作
- 递归
(我记得之前写递归与分治的时候就写过快排)
public static int Partition(int []arr,int left,int right){
int temp=arr[left];
while (left<right){
while (left<right&&arr[right]>temp)--right;
arr[left]=arr[right];
while (left<right&&arr[left]<=temp)++left;
arr[right]=arr[left];
}
arr[left]=temp;
return left;
}
public static void QuickSort(int []arr,int left,int right){
if(left<right){
int pos=Partition(arr,left,right);
QuickSort(arr,left,pos-1);
QuickSort(arr,pos+1,right);
}
}
当我按照思想写出代码的时候,感觉很懵;
这就是递归,代码简洁,不容易理解;
然后我进行一步步的调试;
理清楚了;
int arr[]={1,5,6,8,2,3};
QuickSort(arr,0,arr.length-1);
测试用例
上图将调用顺序画的很清楚,可以看到程序跑完,所有的都排好序了,
而每进行一次Partition函数,就确定一个pos的位置,同时pos左边的都是小于ar[pos]的,右边的全是大于pos的;
每次控制参数的时候,pos-1,pos+1;因为每次排序的时候pos位置确定了,直接缩小范围就行了;
递归的调用其实就是树;
在使用递归的时候有时不需要那么抠细节,明白每个函数的意义;
QuickSort(arr,left,pos-1);排pos左边
QuickSort(arr,pos+1,right);排pos右边
二、交换排序
1、简单选择排序
选择排序的特点:
- 不稳定;
- 无论对什么数据进行排序都是O(n^2)的时间复杂度(较大)
- 数据规模越小越好
算法描述
首先在未排序的序列中找到最小(或最大)的元素(按需查找),存放在排序序列的起始位置,然后,再从剩余的元素中继续寻找最小(或最大)的元素,和已排序好的队列末尾进行交换,以此类推,直到所有元素都排序完成。
重点:
数据结构区分为已排序和未排序,注意下标控制。
注意选择的条件判定
代码实现
笔试题中经常有排序的顺序问题
初始:5,8,7,3,2
1:2,8,7,3,5
2:2,3,7,8,5
3:2,3,5,8,7
4:2,3,5,7,8
public class SelectSort {
public static void selectSort(int []arr){
if(arr==null||arr.length==0){
return;
}
for(int i=0;i<arr.length;i++){
int minIndex=i;
for(int j=i;j<arr.length;j++){
if(arr[j]<arr[minIndex]){
minIndex=j;
}
}
swap(arr,minIndex,i);
}
System.out.println(Arrays.toString(arr));
}
private static void swap(int[] arr, int minIndex, int i) {
int temp=arr[minIndex];
arr[minIndex]=arr[i];
arr[i]=temp;
}
public static void main(String[] args) {
int []arr={5,8,7,3,2};
selectSort(arr);
}
这个代码很容易理解,但处理很巧妙,掌握边界的控制方法
算法分析
时间复杂度:
最优时间复杂度:O(n^2)
平均时间复杂度:O(n^2)
最坏时间复杂度:O(n^2)
空间复杂度:
平均空间复杂度:O(1)
稳定性:不稳定
2、堆排序
堆是一颗顺序存储的完全二叉树。完全二叉树中所有非终端节点的值均不大于(或不小于)其左右孩子节点的值。
小根堆:其中每个节点的值小于等于其左右孩子的值
大根堆:其中每个节点的值大于其左右孩子的值
堆排序就是利用堆这种数据结构进行排序算法。堆排序利用了大根堆(或小根堆)堆顶记录关键字最大(或最小)这一特征,使得在当前无序区中**选取最大(或最小)**关键字的记录变得简单
算法描述
- 先将初始数组建成一个大根堆,此堆为初始的无序
- 再将关键字最大的记录(堆顶)和无序区的最后一个记录交换,此时得到新的无序区和有序区。调整后的堆可能违反堆结构的性质,再将当前无序区调整为堆。然后循环
- 直到无序区只有一个元素为止
堆排序首先是根据元素构建堆。然后将堆的根节点取出(一般是与最后一个节点进行交换),将前面len-1个节点继续进行堆调整的过程,然后再将根节点取出,这样一直到所有节点取出。
代码实现
基本思想:
一定要注意父节点和子节点的下标之间的关系;
i 2i+1 2i+2
注意控制下标
public static void heapSort(int []arr){
if(arr==null){
return;
}
for(int i=(arr.length-1-1)/2;i>=0;i--){
//i代表要调整的根节点的下标
adjust(arr,i,arr.length-1);
//构建大根堆
}
for(int i=0;i<arr.length;i++) {
//0下标 根 保存最大值
//arr[0]和堆中
swap(arr, 0, arr.length - 1-i);//控制下标
adjust(arr,0,arr.length-1-i-1);
}
System.out.println(Arrays.toString(arr));
}
private static void adjust(int[] arr, int start, int end) {
int temp=arr[start];
for(int i=2*start+1;i<=end;i=2*i+1){
if(i+1<=end&&arr[i]<arr[i+1]){
i++;//保存左右孩子较大值的下标
}
if(temp<arr[i]){
//父节点小于孩子节点的值,交换
arr[start]=arr[i];
//选取孩子节点的左孩子节点,继续向下筛选
start=i;
}else {
break;
}
}
arr[start]=temp;//将值赋回来
}
private static void swap(int[] arr, int i, int j) {
int temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
public static void main(String[] args) {
int []arr={5,8,6,3,2,1,4,8,9};
heapSort(arr);
}
算法分析
时间复杂度分析:
最优时间复杂度:O(nlog2n)
平均时间复杂度:O(nlog2n)
最坏时间复杂度:O(nlog2n)
空间复杂度分析:
O(1)
稳定性分析:
不稳定
三、插入排序
1、直接插入排序
通过构建有序序列,对于未排序的数据,在已排序的序列中从后向前进行查找操作,找到满足条件的元素之后进行位置插入操作
(为什么不从前向后进行查找相应的位置呢)
算法描述
- 从第一个元素开始,该元素可以认为已经是排序好的元素;
- 取出下一个元素,在已经排序好了的元素中从后向前进行查找
- 如果已经排序好的元素大于新元素,将该元素移动到下一个位置;
- 重置上一步,直到找到已排序的元素小于或等于新元素,进行该元素的插入动作
代码实现
public static void insertSort(int []arr){
if(arr==null||arr.length==0){
return;
}
//
for(int i=0;i<arr.length;i++){
int minIndex=i;
for(int j=i+1;j<arr.length;j++){
if(arr[j]<arr[minIndex]){
minIndex=j;
}
}
int temp=arr[minIndex];
int z=0;
for( z=minIndex;z>i;z--){
arr[z]=arr[z-1];
}
arr[z]=temp;
}
}
算法分析
时间复杂度分析:
最优时间复杂度:O(n)
平均时间复杂度:O(n^2)
最差时间复杂度:O(n^2)
空间复杂度:
平均空间复杂度:O(1)
稳定性分析:
稳定
2、希尔排序
希尔排序也是一种插入排序,它是简单插入排序的一种算法改进方式。
也成为见效增量排序,希尔排序的时间复杂度相比直接插入排序的时间复杂度要小
他与直接插入排序的不同在于它会优先比较距离远的元素。
希尔排序是按照一定的增量进行分组排序,对每一组进行直接插入排序,随着分组个数的减少,每组中的元素就会越来越多,当增量减少为1时,排序结束。
算法描述
选择增量gap=length/2,缩小增量继续以gap=gap/2的方式进行分组。(经常选5,3,1)
- 选择一个增量序列,按照增量序列个数为m,进行m趟排序;
- 每趟排序根据对应的增量次数分别进行元素的分组操作,对组内进行直接插入排序操作;
- 继续下一个增量,分别进行分组直接插入操作;
- 重复第三个步骤,直到增量变为1,所有元素在一个分组内,希尔排序结束
例:26,53,67,48,57,13,48,32,60,50
增量5,3,1
1:13,48,32,48,50,26,53,67,60,57
2:13,26,32,48,50,48,53,57,60,67
3:13,26,32,48,48,50,53,57,60,67
代码实现
public static void shell(int []arr,int gap){
for(int i=gap;i<arr.length;i++){
int temp=arr[i];
int j=0;
for( j=i-gap;j>=0;j-=gap){
if(temp<arr[j]){
arr[j+gap]=arr[j];
}else {
break;
}
}
arr[j+gap]=temp;
}
}
public static void shellSort(int []arr){
int []partition={5,3,1};
for(int i=0;i<partition.length;i++){
shell(arr,partition[i]);
}
System.out.println(Arrays.toString(arr));
}
public static void main(String[] args) {
int []arr={5,8,6,3,2,1,4,9};
shellSort(arr);
}
算法分析
时间复杂度分析:
平均时间复杂度:O(n1.3~n1.5)
空间复杂度分析:
O(1)
稳定性分析:
不稳定
四、归并排序
归并排序建立在归并的有效操作上进行排序,主要采用分治法将已有序的子序列合并,得到完全有序的序列,即先让每一小段有序,再让小段之间变得有序。若将两个有序合成一个有序段,这又称为二路归并。
算法描述
- 开始以间隔为1的进行归并,也就是说,第一个元素跟第二个进行归并。第三个与第四个进行归并;(每一个元素是有序的)
- 然后,再以间隔为2的进行归并,1-4进行归并,5-8进行归并;
- 再以22的间隔,同理,直到2k超过数组长度为止;
- 当不够两组进行归并的时候,如果超过k个元素,仍然进行归并;如果剩余元素不超过k个元素,那么直接复制给中间数组。
代码实现
public static void mergeSort(int []arr){
int []brr=new int[arr.length];
mergePass(arr,brr,0,arr.length-1);
System.out.println(Arrays.toString(arr));
}
private static void mergePass(int[] arr, int[] brr, int left, int right) {
if(left<right){
int mid=(right-left)/2+left;
mergePass(arr,brr,left,mid);
mergePass(arr,brr,mid+1,right);
merge(brr,arr,left,mid,right);
copy_Arr(arr,brr,left,right);
}
}
private static void merge(int[] brr, int[] arr, int left, int mid, int right) {
int i=left,j=mid+1;
int k=left;
while (i<=mid&&j<=right){
brr[k++]=arr[i]<arr[j]?arr[i++]:arr[j++];
}
while (i<=mid){
brr[k++]=arr[i++];
}
while (j<=right){
brr[k++]=arr[j++];
}
}
private static void copy_Arr(int[] arr, int[] brr, int left, int right) {
for(int i=left;i<=right;i++){
arr[i]=brr[i];
}
// for(int i=0;i<arr.length;i++){
// arr[i]=brr[i];
// }
}
public static void main(String[] args) {
int []arr={5,7,8,9,3,6,1,2};
mergeSort(arr);
}
算法分析
时间复杂度分析:
平均时间复杂度:O(nlog2n)
最坏时间复杂度:O(nlog2n)
空间复杂度分析:O(n)
稳定性分析:
不稳定