搜索算法
写在前面
搜索算法总的来讲就是宽搜和广搜,这篇文章主要来和大家一起做一下leedcode中涉及到深搜和宽搜的题目。
这里以二叉树为列讲一下基础架构的伪代码逻辑,对我们做其它延伸类题目很有帮助。
基础深搜伪代码
public void dfs(Tree node){
//递归退出条件
if(node == null){
return;
}
//递归遍历分支节点
dfs(node.left);
dfs(node.right);
}
基础宽搜伪代码
pulic bfs(Tree node){
//队列存储遍历节点
Queue nodeQ = new LinkedList();
nodeQ.offer(node);
while(!nodeQ.isEmpty()){
Tree temp = nodeQ.poll();
//业务
Tree left = temp.left;
Tree right = temp.right;
nodeQ.offer(left);
nodeQ,offer(right);
}
}
上面两个是最基础的二叉树深搜和宽搜,做下面的这些题的前提就是要理解上面的逻辑。
LeetCode 200 岛屿数量
思路:
- 题目的意思就是求有多少个独立的岛屿,这里的岛屿在题目里面的意思是被0分割的1的块。
- 那么就变成了,怎么通过一个点找到该点相邻的其它点。题目中说了,每个岛屿的邻居是上下左右四个点,那就是说我们遍历一个点的时候,如果想遍历它的相邻节点就要遍历四个点。 我们回到二叉树的遍历,二叉树遍历可以理解为遍历相邻的两个节点。
- 遍历几个节点知道了,我们就要判断遍历的终止条件。 从题目可知,当为0时说明遇到了水,遍历结束。当节点的坐标超出了给出的范围结束。
- 由于树的遍历一直往下,不可能遍历之前的节点,所以不存在死循环。但是这道题的遍历可能出现遍历1节点,2节点是1的邻居,而2节点的邻居又是1,这样就死循环,所以我们需要一个标记矩阵来记录每个点是否标记过,防止死循环。
代码:
class Solution {
public int numIslands(char[][] grid) {
int length = grid.length;
int width = grid[0].length;
//标记函数
int[][] marks = new int[length][width];
int res = 0;
//遍历所有节点,每一个节点深度遍历,找到邻接点
for(int i =0; i<grid.length; i++){
for(int j = 0; j<grid[i].length; j++){
//只有没遍历的点和为陆地的点
if(marks[i][j] == 0 && grid[i][j]=='1'){
dfs(i,j,marks,grid);
res++;
}
}
}
return res;
}
public void dfs(int x,int y, int[][] marks, char[][]grid){
//标记该点遍历过了
marks[x][y] =1;
final int[] x_offset ={-1,1,0,0};
final int[] y_offset ={0,0,-1,1};
//深度遍历四个临界点就等于树的两个孩子遍历,只不过这里是for循环,也可以四个单独
for(int i = 0 ; i <4; i++){
int new_x = x+x_offset[i];
int new_y = y+y_offset[i];
//边界条件
if(new_x >= grid.length || new_x < 0 || new_y >=grid[x].length||new_y < 0){
continue;
}
//边界条件
if(marks[new_x][new_y] == 0 && grid[new_x][new_y] == '1'){
dfs(new_x,new_y,marks,grid);
}
}
}
}
LeetCode 127 单词接龙
思路:
- 一个单词转换另一个单词可以看成一个点到另一个点,那么从begin到end的变化过程就是相当于begin顶点到end顶点的遍历过程。
- 那么问题就变成了,把wordlist 变成一个图,采用宽搜遍历从begin找到end就可
代码:
class Solution {
//内部类,记录每一个字符串,和走了几步
static class Pair{
String key;
int value;
Pair(String key,int value){
this.key = key;
this.value = value;
}
}
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
int res = 0;
Queue<Pair> wordQueue = new LinkedList<>();
//图,key=顶点,value = 边
Map<String,List<String>> grah = new HashMap<>();
//标记是不是访问过了
Set<String> visited = new HashSet<>();
//图的初始化
initGraph(beginWord,wordList,grah);
//宽度遍历
wordQueue.offer(new Pair(beginWord,1));
visited.add(beginWord);
while(!wordQueue.isEmpty()){
Pair firstP = wordQueue.poll();
String word = firstP.key;
int step = firstP.value;
if(word.equals(endWord)){
return step;
}
List<String> edges = grah.get(word);
if(edges != null){
for(String edge : edges){
if(!visited.contains(edge)){
Pair p2 = new Pair(edge,step+1);
wordQueue.offer(p2);
visited.add(edge);
}
}
}
}
return res;
}
//判断两个单词是不是可以转换
public boolean isConnect(String first,String second){
int count = 0;
for(int i = 0; i<first.length(); i++){
if(first.charAt(i)!=second.charAt(i)){
count++;
}
}
return count == 1;
}
//图的初始化
public void initGraph(String beginWord, List<String> wordList,Map<String,List<String>> grah){
wordList.add(beginWord);
for(int i = 0; i < wordList.size(); i++){
for( int j = i+1; j<wordList.size();j++){
String wordI = wordList.get(i);
String wordJ = wordList.get(j);
if(isConnect(wordI,wordJ)){
List<String> valueI = grah.get(wordI);
List<String> valueJ = grah.get(wordJ);
if(valueI == null){
List<String> tempI = new ArrayList<>();
tempI.add(wordJ);
grah.put(wordI,tempI);
}else{
List<String> finalTempI = new ArrayList<>();
finalTempI.addAll(valueI);
finalTempI.add(wordJ);
grah.put(wordI,finalTempI);
}
if(valueJ == null){
List<String> tempJ = new ArrayList<>();
tempJ.add(wordI);
grah.put(wordJ,tempJ);
}else{
List<String> finalTempJ = new ArrayList<>();
finalTempJ.addAll(valueJ);
finalTempJ.add(wordI);
grah.put(wordJ,finalTempJ);
}
}
}
}
}
}
LeetCode 473 火柴拼正方形
思路:
- 这个问题可以理解为我们现在有四个有边界的数组,我们需要按照一定条件从我们的火柴集合里面挑选火柴放入到四个数组里面,并且要保证四个数组都放满,火柴都用掉。
- 其实最容易想到的就是回溯法,我往里面放,如果发现不满足,我们把放的火柴拿出来,尝试往别的数组里面放。如果四个数组都不行,那么返回false;
- 显然这个思路,是一个n4次方的算法,这里我们就要对其进行剪枝(就是找到限制条件,提前返回递归函数),首先,如果火柴总数量<4 直接返回false. 其次,如果火柴长度总和对4取余不等于0返回false,在就是如果放置过程中,如果一个数组里面的长度大于指定长度。直接跳过该数组。
- 如果将火柴都放入进去了,这时候要判断每一个边的长度是不是都等于规定长度。
代码:
class Solution {
public boolean makesquare(int[] nums) {
if (nums.length < 4) return false;
int sum = 0;
for (int num : nums) sum += num;
if (sum % 4 != 0) return false;
//排序,从大的火柴开始放,这样会增加前面剪枝的概率,降低复杂度
Arrays.sort(nums);
return allocate(nums, nums.length - 1, new int[4], sum / 4);
}
private boolean allocate(int[] nums, int pos, int[] sums, int avg) {
if (pos == -1)//如果火柴放完了
return sums[0] == avg && sums[1] == avg && sums[2] == avg;
//四个边的深搜
for (int i = 0; i < 4; ++i) {
//如果这个边放不进去了 就换下一个边
if (sums[i] + nums[pos] > avg) continue;
sums[i] += nums[pos];
//放进去了,递归放下一根火柴
if (allocate(nums, pos - 1, sums, avg))
//如果放进去了,并且满足条件,返回true,如果不满足(也就是火柴都放了,不够/或者火柴放四个边都太大了)
return true;
//回溯,把放进去的火柴拿出来
sums[i] -= nums[pos];
}
return false;
}
}
LeetCode 407 接雨水 II
思路:
- 本题的思路其实比实现更困难,首先,我们要有抽象能力,能够二维转三维。其实这个题的关键是要理解什么情况的数组能够积水,首先积水的现行条件一定至少三行或者三列,两行两列肯定接不住水。
- 只能是中间那排接水,并且要四周的都比他高。这条比较重要,只能中间接水那我们可以理解为四周是不可能接水的,那我们是不是可以考虑先从四周来看整体。并且还有一点只有四周都比中间的高中间的才可以积水,那么两条可以理解为先遍历四周,四周的遍历按照从低到高,那么这时只要保证四周对应的中间的点比它矮那么中间的点就一定可以积水。因为你从低到高遍历,保证了周围的点肯定大于你现在遍历的点,然后你遍历的点的临界点如果比它还矮那么它一定可以积水。
- 如果中间的节点确定可以积水,那么把它的高度也变成和其之前周围节点的高度,并且把它放入队列进行广度优先遍历,因为它也可能成为它内部点的周围节点。
代码:
class Solution {
//记录x,y,h的数据结构
static class node{
int x ;
int y;
int height;
public node(int x,int y,int height){
this.x = x;
this.y = y;
this.height = height;
}
}
public int trapRainWater(int[][] heightMap) {
//三行以下存不了水
if(heightMap.length<3||heightMap[0].length<3) return 0;
//因为我们要从低到高的遍历,所以需要优先级队列数据结构
Queue<node> nodeQ = new PriorityQueue<node>((o1,o2)->o1.height -o2.height);
int row = heightMap.length;
int column = heightMap[0].length;
int[][] mark = new int[row][column];
//将周围的节点放进去
for(int i =0 ; i<row;i++){
mark[i][0] =1;
nodeQ.offer(new node(i,0,heightMap[i][0]));
mark[i][column-1] = 1;
nodeQ.offer(new node(i,column-1,heightMap[i][column-1]));
}
//将周围的节点放进去
for(int j =1; j<column-1;j++){
mark[0][j] =1;
nodeQ.offer(new node(0,j,heightMap[0][j]));
mark[row-1][j] =1;
nodeQ.offer(new node(row-1,j,heightMap[row-1][j]));
}
//遍历节点四周节点的方向数组
int[] x_rail = {-1,1,0,0};
int[] y_rail = {0,0,-1,1};
int res = 0;
//遍历队列,从低到高
while(!nodeQ.isEmpty()){
int x = nodeQ.peek().x;
int y = nodeQ.peek().y;
int h = nodeQ.peek().height;
nodeQ.poll();
//判断边界四周节点的高度是不是比它矮
for(int i = 0 ; i<4; i++){
int new_x = x+x_rail[i];
int new_y = y+y_rail[i];
//如果被遍历过或者不在超出边界直接return
if(new_x < 0||new_x>=row||new_y<0||new_y>=column||(mark[new_x][new_y]==1)) continue;
//如果边界高度高于里面节点的高度,那么一定可以存水
if(h>heightMap[new_x][new_y]){
res+=h-heightMap[new_x][new_y];
//存水之后,高度就变成了水的高度
heightMap[new_x][new_y]=h;
}
//广度遍历,把遍历的中间节点也放到队列
nodeQ.offer(new node(new_x,new_y,heightMap[new_x][new_y]));
//标记为已读
mark[new_x][new_y] = 1;
}
}
return res;
}
}