Java&LeetCode 数据结构——队列. 打开转盘锁
文内代码全部采用JAVA语言。需要提前阅读leetcode上的相关内容。
队列学习
队列遵循先入先出原则,有两个重要的操作,入队 enqueue 和出队 dequeue。队尾入,队头出。java中有内置库,无需重写。
下面是使用内置队列库及其常见操作的一些示例:
// "static void main" must be defined in a public class.
public class Main {
public static void main(String[] args) {
// 1. Initialize a queue.产生新队列
Queue<Integer> q = new LinkedList();
// 2. Get the first element - return null if queue is empty.
//获取第一个元素,也就是整个队列的头,如果返回null说明队列为空
System.out.println("The first element is: " + q.peek());
// 3. Push new element.队尾添加新元素
q.offer(5);
q.offer(13);
q.offer(8);
q.offer(6);
// 4. Pop an element.移除队头的元素,也就是peek()到的元素
q.poll();
// 5. Get the first element.
System.out.println("The first element is: " + q.peek());
// 7. Get the size of the queue.队列长度
System.out.println("The size is: " + q.size());
}
}
广度优先搜索 - 模板
队列经常用于找最短路径的情况。模板就不贴了,leetcode上都有,自行学习。值得注意的是,如果要确保永远不会访问同一个节点的时候,需要增加Set集合。链接如下:
https://leetcode-cn.com/explore/learn/card/queue-stack/217/queue-and-bfs/870/
题目-打开转盘锁
你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字: ‘0’, ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’ 。每个拨轮可以自由旋转:例如把 ‘9’ 变为 ‘0’,‘0’ 变为 ‘9’ 。每次旋转都只能旋转一个拨轮的一位数字。
锁的初始数字为 ‘0000’ ,一个代表四个拨轮的数字的字符串。
列表 deadends 包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。
字符串 target 代表可以解锁的数字,你需要给出最小的旋转次数,如果无论如何不能解锁,返回 -1。
测试用例
示例 1:
输入:deadends = ["0201","0101","0102","1212","2002"], target = "0202"
输出:6
解释:
可能的移动序列为 "0000" -> "1000" -> "1100" -> "1200" -> "1201" -> "1202" -> "0202"。
注意 "0000" -> "0001" -> "0002" -> "0102" -> "0202" 这样的序列是不能解锁的,
因为当拨动到 "0102" 时这个锁就会被锁定。
示例 2:
输入: deadends = ["8888"], target = "0009"
输出:1
解释:
把最后一位反向旋转一次即可 "0000" -> "0009"。
示例 3:
输入: deadends = ["8887","8889","8878","8898","8788","8988","7888","9888"], target = "8888"
输出:-1
解释:
无法旋转到目标数字且不被锁定。
示例 4:
输入: deadends = ["0000"], target = "8888"
输出:-1
个人解法
在最短路径搜索的过程中有很重要的一步:找neighbor,也就是找下一个节点。在这个问题中也就是旋转一下锁盘之后可能的结果,注意:已经尝试过的数值不再尝试,避免陷入无限循环。
首先初始化step,即旋转次数。
public int openLock(String[] deadends, String target) {
int step=0;//旋转次数
//将deadends转为list,方便判断字符串是否是禁忌字符串
List<String> list=Arrays.asList(deadends);
Queue<String> queue=new LinkedList<>();//生成队列
Set<String> used=new HashSet<>();//存放已经尝试过的密码值
queue.add("0000");//初始密码,也就是根节点
used.add("0000");//“0000”已经尝试,不再允许使用
while (queue.isEmpty()==false) {
int size=queue.size();//队列长度
for (int i = 0; i < size; i++) {
String cur=queue.peek();//当前尝试的密码
if (cur.equals(target)) {
return step;//如果当前密码与target相同,则成功,直接返回旋转次数
}
String[] neib=neighbour(cur);//生成邻居,也就是旋转一次之后可能的字符串
for (String str1 : neib) {
//对邻居检查,如果没有使用过并且不是禁忌字符串,则放入队列
if (used.contains(str1)==false && list.contains(cur)==false) {
queue.add(str1);
used.add(str1);
}
}
queue.poll();//已经尝试过的上层节点释放
}
step=step+1;
//尝试结束之后部署+1;因为进入队列的数需要下一次循环才能判断是否是正确密码
}
return -1;//如果没有返回值说明打开失败
}
public String[] neighbour(String a) {
//生成邻居,一个字符串有4位数字,每位数字可以增加1,或减少1,所以总共有4*2=8个邻居
String ans[]=new String[8];
for (int i = 0; i < 4; i++) {
char[] aa=a.toCharArray();
int a1=aa[i]-'0';
if (a1!=0) {
aa[i]=Character.forDigit(a1-1, 10);
}else {
aa[i]='9';
}
ans[i*2]=String.valueOf(aa);
if(a1!=9) {
aa[i]=Character.forDigit(a1+1, 10);
}else {
aa[i]='0';
}
ans[i*2+1]=String.valueOf(aa);
}
return ans;
}
个人解法感觉还是比较好理解,只要能理解leetcode上面提供的模板就行。但是执行起来速度堪忧,289ms,打败23.04%。下面学习一下大神的解法
大神解法
最快解法
执行用时: 44 ms, 在Open the Lock的Java提交中击败了96.20% 的用户
class Solution {
public int openLock(String[] deadends, String target) {
boolean[] isVisit=new boolean[10000];//0~9999共10000种可能
boolean[] isDead=new boolean[10000];
for (String deadEnd : deadends) {
int deadNum = Integer.parseInt(deadEnd);//字符串转十进制数,“0101”=101;
isDead[deadNum] = true;//标记deadends里的数字是禁忌数
}
//如果"0000"在deadEnds中,返回-1
if(isDead[0]) {
return -1;
}
if("0000".equals(target)) {
return 0;
}
int step=0;
Queue<Integer> queue=new ArrayDeque<>();
queue.offer(Integer.parseInt(target));//放入target作为根节点,查找到“0000”退出
isVisit[Integer.parseInt(target)]=true;//标记target已经访问
int last=Integer.parseInt(target);//当前最后一个数
int cenglast=last;
while(!queue.isEmpty())
{
while(true)
{
int[] neighbor=new int[8];
int head=queue.poll();
if(head==0) {
//查找到“0000”退出
return step;
}
int[] nei=neighbor(head);//邻居节点
for(int i=0;i<8;i++)
{
int trans=nei[i];
if(isVisit[trans] || isDead[trans]) {
//如果邻居节点已经访问或者是禁忌数,则跳过下面的步骤,直接i++
continue;
}
//如果不是将这个数放入队列
queue.offer(trans);
last=trans;
isVisit[trans]=true;//标记为已尝试
}
if(head==cenglast) {
//如果当前的节点和上一层的最后一个节点相同,说明已经遍历了这一整层
//退出第二层while循环
break;
}
}
step++;
cenglast=last;
}
return -1;
}
private int[] neighbor(int code)
//转成int型之后用 +9,+1来取余数,+9相当于-1,还避免了用if来求解。
{
int[] res=new int[8];
int a=code%10;
int b=(code/10)%10;
int c=(code/100)%10;
int d=(code/1000)%10;
res[0]=d*1000+c*100+b*10+(a+10-1)%10;
res[1]=d*1000+c*100+b*10+(a+1)%10;
res[2]=d*1000+c*100+((b+10-1)%10)*10+a;
res[3]=d*1000+c*100+((b+1)%10)*10+a;
res[4]=d*1000+((c+10-1)%10)*100+b*10+a;
res[5]=d*1000+((c+1)%10)*100+b*10+a;
res[6]=((d+10-1)%10)*1000+c*100+b*10+a;
res[7]=((d+1)%10)*1000+c*100+b*10+a;
return res;
}
}
双向广度优先搜索(作者:青云算法,非队列解法)
作者:青云算法
来源:CSDN
原文:https://blog.csdn.net/QingyunAlgo/article/details/80589440
版权声明:本文为博主原创文章,转载请附上博文链接!
这个问题是单个源点(“0000”)单个目标(输入的target)的广度优先搜索。我们可以想象当我们到达目标节点的时候,我们同时还遍历了求解树中位于同一层的其他节点。尽管在那一层中我们只需要遍历一个节点,我们却实际上遍历很多不必要的节点,因此单向搜索是存在优化空间的。
我们可以把单项搜索改为双向搜索,也就是既从源点出发向着目标搜索,也从目标出发向着源点搜索。如果两个方向搜索最终能够在中间某个位置相遇,那么表明存在从源点到目标的路径。
我们只需要在单向搜索的代码上稍微作些改动,就能实现双向搜索,如下所示:
76 ms, 在Open the Lock的Java提交中击败了86.83% 的用户
class Solution {
public int openLock(String[] deadends, String target) {
Set<String> dead = new HashSet<>(Arrays.asList(deadends));//禁忌字符串集合
Set<String> visited = new HashSet<>();//已访问字符串集合
String init = "0000";
if (dead.contains(init) || dead.contains(target)) {
return -1;//初始密码或目标密码是禁忌值
}
if (target.equals(init)) {
return 0;//初始密码是目标密码
}
Set<String> set1 = new HashSet<>();//存放从初始值衍生的节点
set1.add(init);
Set<String> set2 = new HashSet<>();//存放从目标衍生来的节点
set2.add(target);
int steps = 0;
while (!set1.isEmpty() && !set2.isEmpty()) {
if (set1.size() > set2.size()) {
Set<String> temp = set1;
set1 = set2;
set2 = temp;
}//永远搜索短的那个结集合
Set<String> set3 = new HashSet<>();
for (String cur : set1) {
for (String next : getNexts(cur)) {
//访问邻居节点
if (set2.contains(next)) {
return steps + 1;
}//如果两端相遇,则直接输出
if (!dead.contains(next) && !visited.contains(next)) {
visited.add(next);
set3.add(next);
}
}
}
steps++;
set1 = set3;//新集合覆盖
}
return -1;
}
private List<String> getNexts(String cur) {
List<String> nexts = new LinkedList<>();
for (int i = 0; i < cur.length(); ++i) {
char ch = cur.charAt(i);
char newCh = ch == '0' ? '9' : (char)(ch - 1);
StringBuilder builder = new StringBuilder(cur);
builder.setCharAt(i, newCh);
nexts.add(builder.toString());
newCh = ch == '9' ? '0' : (char)(ch + 1);
builder = new StringBuilder(cur);
builder.setCharAt(i, newCh);
nexts.add(builder.toString());
}
return nexts;
}
}