《剑指offer》专题—算法训练 day01
Posted RAIN 7
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《剑指offer》专题—算法训练 day01相关的知识,希望对你有一定的参考价值。
文章目录
《剑指offer》专题—算法训练 day01
从今天起,博主开始了 《 剑指offer 》 系列 算法专题的学习,希望大家 跟随着博主一起,开始这段美妙的算法之旅…
一、二维数组的查找
题目链接:
https://www.nowcoder.com/practice/abc3fe2ce8e146608e868a70efebf62e?
思路一
暴力算法
分析:直接遍历一遍数组,即可判断目标target是否存在。
public class Solution {
public boolean Find(int target, int [][] array) {
// 先for 循环遍历一下 数组的每一行
for(int i = 0;i<array.length;i++){
// 再 for 循环遍历一下数组这一行的每一列
for(int j = 0;j<array[0].length;j++){
// 判断每一个元素是否是我们需要的target
if(array[i][j] == target){
return true;
}
}
}
return false;
}
}
思路二
查找的过程 本质是 排除的 过程
我们用暴力算法 一次只能排除一个,效率很低
我们可以利用这个题中矩阵的性质
每一行从左到右依次递增
每一列从上到下依次递增
我们会发现右上角的值 是所在行中最大的,同时也是所在列中 最小的.
那么我们每次查找 target 值时,都与这个矩阵 右上角的值进行比较
如果 小于 右上角,那么可以排除这一列
如果 大于 右上角 , 那么可以排除这一行
好了,我们根据这个思路可以写出代码:
public class Solution {
public boolean Find(int target, int [][] array) {
int i = 0;
int j = array[0].length-1;
while(i<=array.length-1 && j>=0){
// array[i][j]一定是当前行最大,当前列最小的数
// target < array[i][j] 排除当前列
if(target<array[i][j]){
j--;
// target> array[i][j] 排除当前行
}else if(target >array[i][j]){
i++;
// target == array[i][j] 此时找到 对应元素,返回true
}else{
return true;
}
}
// 如果循环跳出,那么说明没有找到对应的元素,此时返回 false
return false;
}
}
二、旋转数字的最小数字
题目链接:
https://www.nowcoder.com/practice/9f3231a991af4f55b95579b44b7a01ba?
思路一
本质其实是一个求最小值问题
理论上,遍历一次即可
import java.util.ArrayList;
public class Solution {
public int minNumberInRotateArray(int [] array) {
if(array == null || array.length == 0){
return 0;
}
int min = array[0];
for(int i = 1;i<array.length;i++){
if(min>array[i]){
min = array[i];
}
}
return min;
}
}
思路二
按照要求,要么是一个非递减排序的数组(最小值在最开始),要么是一个旋转(最小值在中间某个地方)
而且,旋转之后有个特征,就是在遍历的时候,原始数组是非递减的,旋转之后,就有可能出现递减,引起递减的数字,就 是最小值
采用二分查找的方式,进行定位
定义首尾下标,因为是非递减数组旋转,所以旋转最后可以看做成两部分,前半部分整体非递减,后半部分整体非递减,前 半部分整体大于后半部分。
所以,我们假设如下定义,left指向最左侧,right指向最右侧,mid为二分之后的中间位置。
则,a[mid] >= a[left],说明mid位置在原数组前半部分,进一步说明,目标最小值,在mid的右侧,让left=mid
a[mid] < a[left], 说明mid位置在原数组后半部分,进一步说明,目标最小值,在mid的左侧,让right=mid
这个过程,会让[left, right]区间缩小
这个过程中,left永远在原数组前半部分,right永远在原数组的后半部分,而范围会一直缩小
两种情况:
当left和right相邻时,right指向的位置,就是最小元素的位置
但是,因为题目说的是非递减,也就意味着数据允许重复,因为有重复发,就可能会有arr[left] == arr[mid] == arr[right]的情况,我们就无法判定数据在mid左侧还是右侧。(注意,只要有两者不相等,我们就能判定应该如何缩小范围)
相关代码:
// 二分查找
import java.util.ArrayList;
public class Solution {
public int minNumberInRotateArray(int [] array) {
// 判断数组是否为空 以及 数组内容是否为空
if(array == null || array.length == 0){
return 0;
}
int left = 0;
int right = array.length-1;
int mid = 0;
while(array[left]>=array[right]){
// 这里的循环条件是 因为是旋转数组所以左区间最小的值 大于等于右区间最大的值
// 这种 情况是 当区间缩小到只有两个元素是,右边那个是最小的数字
if(right-left ==1){
mid = right;
break;
}
mid = (left +right)/2;
// 这种情况是 当 左 中 右 三值都相等时,我们无法判断 mid下标元素在左区间还是右区间
// 我们只能从头遍历,查找数组中的最小值
if(array[left] == array[mid] && array[mid] == array[right]){
int result = array[left];
for(int i = left+1;i<right;i++){
// 我们不需要从 left 开始遍历 ,还有最后 不用 <= right,因为 arr[left] = arr[right] ,我们不用再去判断
if(array[i]<result){
result = array[i];
}
}
return result;
}
//下面的这个判断就是 mid元素 大于 left元素,那么mid 在左区间,而我们要查找的最小数字在右区间,所以left=mid,缩小区间
if(array[mid]>=array[left]){
left = mid;
}else{
right = mid;
}
}
// 最后返回 mid下标的元素
return array[mid];
}
}
三、奇偶互换
题目链接:
https://www.nowcoder.com/practice/beb5aa231adc45b2a5dcc5b62c93f593?
大家做这种题目一定要看好,调换奇数和偶数的时候 ,有没有说明 相对位置是否发生改变.
当然了,这道题原题是不需要保证奇偶位置不变的,先给大家说一下 相对位置发生改变的题目.
相对位置变化
思路
给大家说一下思路:
左右指针法
我们需要定义一个 左指针 和右指针 分别从 数组的两头进行遍历.
在一个 left < right 的一个循环条件下,
左指针从数组的左边开始遍历,遇到偶数就停止,遇到奇数就跳过
右指针从数组的右边开始遍历,遇到奇数就停止,遇到偶数就跳过.
这两边遍历完之后我们会得到 左边遍历得到的偶数下标 ,右边遍历得到的奇数下标,此时交换这两个下标的数字
重复以上操作,我们最后得到了一个 奇数在前 偶数在后 (相对位置发生变化) 的 一个数组序列.
题解代码
import java.util.* ;
// 这是相对位置发生变化的一种做法
public class Solution {
public void reOrderArray(int [] array) {
if(array == null || array.length == 0){
return;
}
// 定义一个左右指针
int left = 0;
int right = array.length-1;
// 分别从数组的两头开始遍历
while(left<=right){
// 从左边开始遍历 ,遇到偶数停止,遇到奇数跳过
while(left<right && array[left]%2 ==1){
left++;
}
// 从右边开始遍历 ,遇到奇数停止,遇到偶数跳过
while(left<right && array[right]%2 == 0){
right--;
}
// 此时我们得到了一个左边是偶数的下标,右边是奇数的下标
// 交换奇数 偶数的排列顺序
if(left <= right){
int tmp = array[left];
array[left] = array[right];
array[right] = tmp;
}
}
}
}
相对位置不变
思路
从左向右,每次遇到的,都是最前面的奇数,一定将来要被放在k下标处,
现将当前奇数保存起来
将该奇数之前的内容(偶数序列),整体向后移动一个位置.
将奇数保存在它将来改在的位置下标(k指向的位置),因为我们是从左往右放的,没有跨越奇 数,所以一定是相对位置不变的
import java.util.* ;
public class Solution {
public void reOrderArray(int [] array) {
if(array == null || array.length == 0){
return;
}
int k = 0;
for(int i = 0;i<array.length;i++){
if(array[i]%2 == 1){
int tmp = array[i];
for(int j = i-1;j>=k;j--){
array[j+1] = array[j];
}
array[k++] = tmp;
}
}
}
}
四、数组中出现次数超过一半的数字
题目链接:
https://www.nowcoder.com/practice/e8a1b01a2df14cb2b228b30ee6a92163?
思路一
思路一:定义map,使用<数字,次数>的映射关系,最后统计每个字符出现的次数
相关代码
import java.util.*;
public class Solution {
public int MoreThanHalfNum_Solution(int [] array) {
Map<Integer ,Integer> map = new HashMap<>();
for(int i =0;i<array.length;i++){
if(map.get(array[i]) == null){
// 如果这个 arr[i] 没有在 map 中出现过的话,那么就往 map 中 存上arr[i],且计数为1
map.put(array[i],1);
}else{
// 这种情况是 arr[i] 出现的次数大于1次,出现很多次
int k = map.get(array[i]);
// 我们先 用 k 来保存这个arr[i] 在map中之前出现的次数
map.put(array[i],k+1);
// arr[i] 存放入 map中,并且次数+1
}
if(map.get(array[i])>array.length/2){
// 如果arr[i] 在 map 中出现的次数超过数组长度的一半,那么直接返回 arr[i]
return array[i];
}
}
// 因为数组中可能0出现次数超过长度一半,这里是为了符合逻辑,并不是真正的业务代码.
return 0;
}
}
思路二
思路二: 排序,出现次数最多的数字,一定在中间位置。然后检测中间出现的数字出现的次数是否符合要求
相关代码
import java.util.*;
public class Solution {
public int MoreThanHalfNum_Solution(int [] array) {
if(array == null || array.length == 0){
return 0;
}
// 我们先将这个数组进行排序,我们可以 用到几种排序方法: 直接插入排序、堆排序、冒泡排序、选择排序等等
for(int i =0;i<array.length;i++){
for(int j = 0;j<array.length-1-i;j++){
if(array[j]>array[j+1]){
int tmp = array[j];
array[j] = array[j+1];
array[j+1] = tmp;
}
}
}
// 如果这个数字出现的次数超过了数组长度的一半,那么数组最中间的那个数字一定是 我们想要得到的数字
int left = 0;
int right = array.length-1;
int mid = (left+right)/2;
int count = 0;
for(int i = 0;i<array.length;i++){
if(array[i] == array[mid]){
count++;
}
}
// 如果中间的数字出现的次数大于 数组长度的一半,那么返回这个数字,
// 如果没有,那么返回0(同样的,0也只是逻辑上的处理,并不是业务的处理)
return count>array.length/2 ? array[mid] : 0;
}
}
思路三
思路三:目标条件:目标数据超过数组长度的一半,那么对数组,我们同时去掉两个不同的数字,到最后剩下的一个数就是该数字。如果剩下两个,那么这两个也是一样的,就是结果),在其基础上把最后剩下的一个数字或者两个回到原来数组中,将数组遍历一遍统计一下数字出现次数进行最终判断。
相关代码
import java.util.*;
public class Solution {
public int MoreThanHalfNum_Solution(int [] array) {
if(array == null || array.length == 0){
return 0;
}
int times = 1;
int target = array[0];
// 下面这个过程很难理解
// 每两个不同的数字 会抵消一次 times
// 如果times == 0,那么说明i之前的数字 都不同,再次更换一个 target
// 到最后 target 保留的数字很可能是 出现次数超过数组长度一半的数字
for(int i = 1;i<array.length;i++){
if(times ==0){
// 如果 times 为0,那么之前不同的抵消完了
target = array[i];
times = 1;
}else if(array[i] == target){
times++;
}else{
times--;
}
}
// 如果输入本身符合条件,那么最后 times 大于0,target 保存的目标就是准目标,但是还需要确认
int count = 0;
for(int i = 0;i<array.length;i++){
if(target == array[i]){
count++;
}
}
return count>array.length/2? target: 0;
}
}
好了,今天的内容就结束了,希望大家多多练习~~
谢谢欣赏!!!
以上是关于《剑指offer》专题—算法训练 day01的主要内容,如果未能解决你的问题,请参考以下文章
代码随想录算法训练营day46|139.单词拆分 剑指Offer10-I.斐波那契数列 10-II.青蛙跳台阶问题
代码随想录算法训练营第8天 | ● 344.反转字符串 ● 541. 反转字符串II ● 剑指Offer 05.替换空格 ● 151.翻转字符串里的单词 ● 剑指Offer58-II.左旋转字符串