查找
二分查找
手写代码
自己语言描述算法
判断查找次数
语言描述
前提:有已排序数组 A(假设已经做好)
定义左边界 L、右边界 R,确定搜索范围,循环执行二分查找(3、4两步)
获取中间索引 M = Floor((L+R) /2)
中间索引的值 A[M] 与待搜索的值 T 进行比较
① A[M] == T 表示找到,返回中间索引
② A[M] > T,中间值右侧的其它元素都大于 T,无需比较,中间索引左边去找,M - 1 设置为右边界,重新查找
③ A[M] < T,中间值左侧的其它元素都小于 T,无需比较,中间索引右边去找, M + 1 设置为左边界,重新查找
当 L > R 时,表示没有找到,应结束循环
模式
有序度比较高的数组通常都会考虑这个方法,哪怕是局部有序
代码
public int binarySearch(int []nums,int target){
int l=0,r=nums.length-1;
int mid=(l+r)>>>1;
while(l<=r){
mid=(r+l)>>>1;
if(nums[mid]==target){
return mid;
}else if(nums[mid]>target){
r=mid-1;
}else{
l=mid+1;
}
}
return -1;
}
查找次数判定
看mid一共有过多少个值,这很好理解
给元素个数:利用公式,取2的对数向下取整
排序
冒泡排序
手写代码
自己语言描述算法
建议一步到位直接优化后的
语言描述
- 依次比较数组中相邻两个元素大小,若 a[j] > a[j+1],则交换两个元素,两两都比较一遍称为一轮冒泡,结果是让最大的元素排至最后
- 比较过程中记录最后一次交换的位置,下一轮只需比较到该位置,如果一次交换都没有,则说明数组已经有序,可以直接返回
- 重复以上步骤,直到整个数组有序
代码
public void bubbleSort(int [] nums){
//记录无序区边界,每次从无序区开始排,其后面的都没有交换过,说明已经有序,那么下次就只需排到这里
int last=-1,n=nums.length-1;
while(true){
last=-1;
for(int j=0;j<n;j++){
if(nums[j]>nums[j+1]){
int tmp = nums[j];
nums[j]=nums[j+1];
nums[j+1]=tmp;
last=j;
}
}
//下次比较的边界是最后一次交换的位置
n=last;
//没有交换过可以直接返回
if(n==-1){
return;
}
}
}
选择排序
手写代码
自己语言描述算法
与冒泡比较
语言描述
将数组分为两个子集,排序的和未排序的,每一轮从未排序的子集中选出最小的元素,放入排序子集
重复以上步骤,直到整个数组有序
代码
public void selectSort(int [] nums){
for(int i=0;i<nums.length-1;i++){
//记录最小元素的索引
int min=i;
for(int j=i+1;j<nums.length;j++){
if(nums[min]>nums[j]){
min=j;
}
}
if(i!=min){
int tmp=nums[i];
nums[i]=nums[min];
nums[min]=tmp;
}
}
}
VS冒泡
二者平均时间复杂度都是O(N^2)
选择排序一般要快于冒泡,因为其交换次数少
但如果集合有序度高,冒泡优于选择
冒泡属于稳定排序算法,而选择属于不稳定排序,后者在交换的时候可能把元素带到后面去
插入排序
手写代码
自己语言描述算法
语言描述
将数组分为两个区域,排序区域和未排序区域,每一轮从未排序区域中取出第一个元素,插入到排序区域(需保证顺序)
重复以上步骤,直到整个数组有序
代码
public void insertSort(int []nums){
for(int i=1;i<nums.length;i++){
int cur=nums[i];
for(int j=i;j>=1;j--){
//如果前一个元素比当前元素大,直接将其后移一位
if(cur<nums[j-1]){
nums[j]=nums[j-1];
}else{
nums[j]=cur;
break;
}
}
}
}
VS选择
二者平均时间复杂度都是O(N^2),插入如果是集合已经有序,则O(N)
插入属于稳定排序算法,而选择属于不稳定排序
大部分情况下,插入都略优于选择
据说插入在小规模排序中是首选
希尔排序
自己语言描述算法
语言描述
首先选取一个间隙序列,如 (n/2,n/4 … 1),n 为数组长度
每一轮将间隙相等的元素视为一组,对组内元素进行插入排序,目的有二
① 少量元素插入排序速度很快
② 让组内值较大的元素更快地移动到后方(改进了插入排序的不足)
当间隙逐渐减少,直至为 1 时,即可完成排序
代码
public void shellSort(int[] nums){
int len=nums.length;
for(int gap=len>>1;gap>=1;gap>>1){
for(int i=gap;i<len;i++){
int j=i;
int cur=nums[i];
for(j=i;j>=gap;j-=gap){
if(nums[j-gap]>cur){
nums[j]=nums[j-gap];
}else{
break;
}
}
nums[j]=cur;
}
}
}
VS插入
非稳定
如果增量序列选得好,时间复杂度为O(N^1.5)
快速排序
自己语言描述
手写单边循环、双边循环
比较罗穆托和霍尔两种分区方案的性能
语言描述
- 每一轮排序选择一个基准点(pivot)进行分区
- 让小于基准点的元素的进入一个分区,大于基准点的元素的进入另一个分区
- 当分区完成时,基准点元素的位置就是其最终位置
- 在子分区内重复以上过程,直至子分区元素个数少于等于 1,这体现的是分而治之的思想 (divide-and-conquer)
- 从以上描述可以看出,一个关键在于分区算法,常见的有洛穆托分区方案、双边循环分区方案、霍尔分区方案
分区方案
单边循环快排(lomuto 洛穆托分区方案)
语言描述
最右边元素为基准点
两个指针i,j,i负责维护比基准点小的元素的边界,j负责从左往右找比基准点小的元素
一旦找到,与i交换位置
直到j指针指向基准点,再次与i交换位置
而后i就是基准点在排序完成后所在位置了,也即此次分区的位置
代码
public int[] lomutoQuick(int[] nums){
int l=0,r=nums.length-1;
partition(nums,l,r);
return nums;
}
public void partition(int[] nums,int l,int r){
if(l>=r){
return;
}
int i=0,j=0;
int base=nums[r];
while(j<r){
while(j<r&&nums[j]>=base){
j++;
}
if(j!=i){
int tmp=nums[i];
nums[i]=nums[j];
nums[j]=tmp;
i++;
}else{
j++;
}
}
int tmp=nums[i];
nums[i]=nums[r];
nums[r]=tmp;
partition(nums,l,i-1);
partition(nums,i+1,r);
return;
}
双边循环快排(不完全等价于 hoare 霍尔分区方案)
语言描述
最左边元素为基准点
两个指针i,j,i从左往右找大于基准点的元素,j负责从右往左找大于基准点的元素
先找j,再找i
找到后交换位置
直到两指针相碰,将基准点元素与之交换位置
j即为此次分区位置
代码
public int [] doubleQuick(int[] nums){
int l=0,r=nums.length-1;
partition(nums,l,r);
return nums;
}
public void partition(int[] nums,int l,int r){
if(l>=r){
return;
}
int base=nums[l];
int i=l,j=r;
while(i<j){
while(i<j&&nums[j]>=base)j--;
while(i<j&&nums[i]<=base)i++;
int tmp=nums[i];
nums[i]=nums[j];
nums[j]=tmp;
}
int tmp=nums[i];
nums[i]=nums[l];
nums[l]=tmp;
partition(nums,i+1,r);
partition(nums,l,i-1);
return;
}
特点
快,平均时间复杂度是 O(nlog_2n ),平均来讲分区分logn次,每次都会操作n个元素,最坏时间复杂度 O(n^2),最坏情况下分区会分n次,那种完全逆序的情况
不稳定
数据量较大时优势极其明显
罗穆托和霍尔分区性能比较
霍尔的移动次数平均来讲比洛穆托少3倍