Byte提前批二面中自己在二分查找的变形题中翻了车,导致现在还不知道面试是否会通过,面试结束后真的感觉无法原谅自己在这么简单的问题上翻车。本以为自己在算法的准备上还算充分,原来是自己好高骛远了,一直在看相对比较难的算法,而没有踏踏实实熟练掌握基础算法。从实习到面试,整个七月里一直被反复教做人 ,踏踏实实学习吧。 |
---|
二分查找的基本思想(二叉搜索树也应该算是二分查找的范畴)
二分查找的基本思想是:(设R[low,high]是当前的查找区间)
(1) 首先确定该区间的中点位置:
(2)然后将待查的K值与R[mid].key比较:若相等,则查找成功并返回此位置,否则须确定新的查找区间,继续二分查找,具体方法如下:
①若R[mid].key>K,则由表的有序性可知R[mid,n].keys均大于K,因此若表中存在关键字等于K的结点,则该结点必定是在位置mid左边的子表R[1,mid-1]中,故新的查找区间是左子表R[1,mid-1]。
②类似地,若R[mid].key
二分查找的应用
数组中找到局部最小的值
数字在排序数组中出现的次数
在有序旋转数组中找到最小值
在有序旋转数组中找到一个数
最小的k个数-这里partaton与二分结合的方法时间复杂度和空间复杂度最优
刷题的过程中发现,排序算法中快排、堆排和归并思想使用得最多,
选取一个关键字(key)作为枢轴,一般取整组记录的第一个数/最后一个,这里采用选取序列最后一个数为枢轴,也是初始的坑位。
设置两个变量left = 0;right = N - 1;
从left一直向后走,直到找到一个大于key的值,然后将该数放入坑中,坑位变成了array[left]。
right一直向前走,直到找到一个小于key的值,然后将该数放入坑中,坑位变成了array[right]。
重复3和4的步骤,直到left和right相遇,然后将key放入最后一个坑位。
class Solution {
public int[] sortArray(int[] nums) {
int left=0;
int right=nums.length-1;
quickSort(nums,left,right);
return nums;
}
public void quickSort(int[] array,int left,int right){
if(left >= right)//表示已经完成一个组
{
return;
}
int index = partation(array,left,right);//枢轴的位置
quickSort(array,left,index - 1);
quickSort(array,index + 1,right);
}
public int partation(int[] array,int left,int right){
int key = array[right];
while(left < right) {
while(left < right && array[left] <= key)
{
++left;
}
array[right] = array[left];
while(left < right && array[right] >= key)
{
--right;
}
array[left] = array[right];
}
array[right] = key;
return left;
}
}
快排的应用(其实就是partation的应用)
经典荷兰国旗问题
堆是一种数据结构,一种叫做完全二叉树的数据结构。大顶堆:每个节点的值都大于或者等于它的左右子节点的值。小顶堆:每个节点的值都小于或者等于它的左右子节点的值。对于大顶堆:arr[i] >= arr[2i + 1] && arr[i] >= arr[2i + 2]
,对于小顶堆:arr[i] <= arr[2i + 1] && arr[i] <= arr[2i + 2]。
堆排序的基本思想是:1、将带排序的序列构造成一个大顶堆,根据大顶堆的性质,当前堆的根节点(堆顶)就是序列中最大的元素;2、将堆顶元素和最后一个元素交换,然后将剩下的节点重新构造成一个大顶堆;3、重复步骤2,如此反复,从第一次构建大顶堆开始,每一次构建,我们都能获得一个序列的最大值,然后把它放到大顶堆的尾部。最后,就得到一个有序的序列了。
import java.util.*;
class Solution {
public int[] sortArray(int[] nums) {
// Arrays.sort(nums);
heapSort(nums);
return nums;
}
public void heapSort(int[] arr) {
if (arr == null || arr.length == 0) {
return;
}
int len = arr.length;
// 构建大顶堆,这里其实就是把待排序序列,变成一个大顶堆结构的数组
buildMaxHeap(arr, len);
// 交换堆顶和当前末尾的节点,重置大顶堆
while(len>0){
swap(arr,0,len-1);
heapify(arr,0,--len);
}
}
private void buildMaxHeap(int[] arr, int len) {
// 从最后一个非叶节点开始向前遍历,调整节点性质,使之成为大顶堆
for(int i=(int)len/2;i>=0;i--){
heapify(arr,i,len);
}
}
private void heapify(int[] nums, int i, int len) {
// 先根据堆性质,找出它左右节点的索引
int left = 2 * i + 1;
int right = 2 * i + 2;
// 默认当前节点(父节点)是最大值。
int largestIndex = i;
if (left < len && nums[left] > nums[largestIndex]) {
// 如果有左节点,并且左节点的值更大,更新最大值的索引
largestIndex = left;
}
if (right < len && nums[right] > nums[largestIndex]) {
// 如果有右节点,并且右节点的值更大,更新最大值的索引
largestIndex = right;
}
if (largestIndex != i) {
// 如果最大值不是当前非叶子节点的值,那么就把当前节点和最大值的子节点值互换
swap(nums, i, largestIndex);
// 因为互换之后,子节点的值变了,如果该子节点也有自己的子节点,仍需要再次调整。
heapify(nums, largestIndex, len);
}
}
private void swap (int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
堆排序的应用
堆排序的应用有的时候也是顶堆的应用。
窗口的中位数–利用优先队列
leetcode253. 会议室II(java):最小堆(非常常见的一道题 高频高频)
数据流的中位数
分金条的最小花费
做项目的最大收益
前K个高频元素
前K个高频单词
归并排序(Merge Sort)是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
归并排序应用
import java.util.*;
class Solution {
public int[] sortArray(int[] nums) {
//需要有个辅助数组来保存归并的结果
int[] help=new int[nums.length];
mergeSort(nums,help,0,nums.length-1);
return nums;
}
public void mergeSort(int[] arr,int[] help,int left,int right){
//basecase
if(left>=right){
return;
}
int mid=left+(right-left)/2;
mergeSort(arr,help,left,mid);
mergeSort(arr,help,mid+1,right);
merge(arr,help,left,mid,right);
}
public void merge(int[] arr,int[] help,int left,int mid,int right){
int l=left;
int r=mid+1;
int index=left;
//归并的主过程
while(l<=mid&&r<=right){
if(arr[l]<arr[r]){
help[index++]=arr[l++];
}else{
help[index++]=arr[r++];
}
}
//有剩余元素
if(l<=mid){
for(int i=l;i<=mid;i++){
help[index++]=arr[i];
}
}
if(r<=right){
for(int i=r;i<=right;i++){
help[index++]=arr[i];
}
}
//将结果丛辅助元素拷贝回原来的数组
for(int j=left;j<=right;j++){
arr[j]=help[j];
}
}
}
冒泡、插入和选择排序属于时间复杂度N*N的算法,很少使用,希尔排序是插入排序的变形虽然时间复杂度有所改善但是使用得场景也并不多。
冒泡:
class Solution {
public int[] sortArray(int[] nums) {
bubbleSort(nums);
return nums;
}
public void bubbleSort(int[] arr){
if(arr==null){
return;
}
for(int i=0;i<arr.length-1;i++){
for(int j=0;j<arr.length-1-i;j++){
if(arr[j]>arr[j+1]){
int temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
}
}
}
}
}
选择排序
class Solution {
public int[] sortArray(int[] nums) {
selectSort(nums);
return nums;
}
public void selectSort(int[] arr){
for(int i=0;i<arr.length;i++){
int min=i;
for(int j=i+1;j<arr.length;j++){
if(arr[j]<arr[min]){
min=j;
}
}
if(min!=i){
int temp=arr[min];
arr[min]=arr[i];
arr[i]=temp;
}
}
}
}
插入排序
class Solution {
public int[] sortArray(int[] nums) {
insertSort(nums);
return nums;
}
public void insertSort(int[] arr){
if(arr==null){
return;
}
int i=0;
int j=0;
for(i=1;i<arr.length;i++){
int temp=arr[i];
for(j=i;j>0&&arr[j-1]>temp;j--){
arr[j]=arr[j-1];
}
arr[j]=temp;
}
}
}
希尔排序
public void sort(int[] array) {
int number = array.length / 2;
int i;
int j;
int temp;
while (number >= 1) {
for (i = number; i < array.length; i++) {
temp = array[i];
j = i - number;
while (j >= 0 && array[j] > temp) {
array[j + number] = array[j];
j = j - number;
}
array[j + number] = temp;
}
number = number / 2;
}
}
计数排序
计数排序不是比较数值排序,是记录数据出现次数的一种排序算法
找出待排数组中最大值
额外一个数组记录待排数组值出现的次数
循环打印存储数值次数的数组下标值
import java.util.Arrays;
/**
* 计数排序2
* 中间数组 通过数组下标来表示原始数组的值,来统计每个元素出现的次数
* 然后新建一个数组将 中间数组 出现了几次,我就打印几次 到新的数组
*
* 计数排序自己的理解:
* 就是将原始数组中的数值出现的频率(次数)记录在新数组下标中,
* 然后通过遍历循环这个新数组赋值给另一个新数组
*
* 时间复杂度
* 从代码看,第一个for循环时间复杂度是O(k),第二个是O(n),第三个是O(k),第四个是O(n),所以总的是O(k+n),特别当n==k的时候,时间复杂度是O(n)。
* 计数排序不需要比较操作,也不需要交换操作,是一种简单的排序方式,但是这是一种空间换时间的排序方式,类似的空间换时间的排序还有桶排序等。
* 特别的当O(k)>=O(nlogn)的时候,计数排序就不那么有效了。
*/
public class _05CountSortExample {
public static void main(String[] args) {
int[] arr = {2, 5, 3, 0, 2, 3, 0, 3};
int[] arr2 = jspx(arr);
System.out.println(Arrays.toString(arr2));
}
public static int[] jspx(int[] A) {
//一:求取最大值和最小值,计算中间数组的长度:中间数组是用来记录原始数据中每个值出现的频率
int max = A[0], min = A[0];
for (int i : A) {
if (i > max) {
max = i;
}
if (i < min) {
min = i;
}
}
//二:有了最大值和最小值能够确定中间数组的长度
//存储5-0+1 = 6
int[] pxA = new int[max - min + 1];
//三.循环遍历旧数组计数排序: 就是统计原始数组值出现的频率到中间数组B中
for (int i : A) {
pxA[i - min] += 1;//数的位置 上+1
}
//四.遍历输出
//创建最终数组,就是返回的数组,和原始数组长度相等,但是排序完成的
int[] result = new int[A.length];
int index = 0;//记录最终数组的下标
//先循环每一个元素 在计数排序器的下标中
for (int i = 0; i < pxA.length; i++) {
//循环出现的次数
for (int j = 0; j < pxA[i]; j++) {//pxA[i]:这个数出现的频率
result[index++] = i + min;//以为原来减少了min现在加上min,值就变成了原来的值
}
}
return result;
}
}
计数排序的应用:
一般max-min小,数据量又很大就可以尝试计数排序
求高考名次(腾讯面试题)
基数排序