有趣的约瑟夫环问题
问题描述
著名的约瑟夫问题:编号为 1-N 的 N 个士兵围坐在一起形成一个圆圈,从编号为 1 的士兵开始依次报数(1,2,3… 这样依次报数),数到 m 的 士兵会被淘汰出列,之后的士兵再从 1 开始报数。直到最后剩下一个士兵,求这个士兵的编号。
package com.ty;
public class YueSeFu {
public static void main(String[] args) {
int idx = josephus(5, 3);
System.out.println(idx);
}
public static int josephus(int n,int m){
int[] people=new int[n];
int count=0;
int remainNum=n;
int index=-1;
while(remainNum>0){
index++;
if(index==n) index=0;
if (people[index]==-1) continue;
else{
count++;
if (count==m) {
remainNum--;
count=0;
people[index]=-1;
}
}
}
return index;
}
}
生成滑动窗口最大值数组
小明同学的老师给了一道题,假设给定一个整形数组 nums 和一个大小为 k 的窗口,k 小于 nums 的长度,窗口从数组的最左边,每次滑动一个数,一直到最右边,返回每次滑动窗口中的最大值的数组。
暴力法:
public class MaxWindows {
public static void main(String[] args) {
int[] nums = { 3, 5, -1, 3, 2, 5, 1, 6 };
int[] results = MaxWindows.maxSlidingWindow(nums, 3);
for (int result : results) {
System.out.print(result + " ");
}
}
// 暴力
public static int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
if (n == 0 || k == 0) {
return new int[0];
}
int[] results = new int[n - k + 1];
for (int i = 0; i < n - k + 1; i++) {
int max = Integer.MIN_VALUE;
for (int j = i; j < i + k; j++)
max = Math.max(max, nums[j]);
results[i] = max;
}
return results;
}
}
双端队列解法
- 遍历数组 nums[] 中的元素 num[i],执行以下的操作。
- 执行循环:如果队列不为空,且以队列的最后一个元素为下标的数组元素 nums[queue.peekLast()] 小于 num[i] 时,将队列的最后一个元素删除。意为:删除队列中较小的元素索引。
- 将当前元素下标 i 添加到队列的尾部。
- 如果队列的队首元素小于 i-k,则移除队首的元素,说明队首的元素索引已经超过了滑动窗口的长度了,应该抛弃队首的索引。
- 如果 i 大于等于 k-1,那么说明滑动窗口长度已经生效,此时的队列第一个元素作为索引,取出数组中的数值,就是 i-k+1 为起始的滑动窗口的最大值。
import java.util.LinkedList;
public class MaxWindows {
public static void main(String[] args) {
int[] nums = { 3, 5, -1, 3, 2, 5, 1, 6 };
int[] results = MaxWindows.maxSlidingWindow(nums, 3);
for (int result : results) {
System.out.print(result + " ");
}
}
// 滑动窗口
public static int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || nums.length <= 0 || k <= 0) {
return new int[0];
}
int len = nums.length;
int[] results = new int[len - k + 1];
// 双向队列
LinkedList<Integer> queue = new LinkedList<>();
for (int i = 0; i < len; i++) {
// 移除比较小的元素
while (!queue.isEmpty() && nums[queue.peekLast()] < nums[i]) {
queue.removeLast();
}
// 添加当前元素
queue.addLast(i);
// 移除索引不在有效窗口内的元素
if (i - queue.peekFirst() >= k) {
queue.removeFirst();
}
// 计算窗口内的数据
if (i >= k - 1) {
results[i - k + 1] = nums[queue.peekFirst()];
}
}
return results;
}
}
找链表的倒数 k 个元素
public class ListNode {
// 属性
int val;
// 下一个节点的引用
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
那么如果访问链表的顺序第 k 个节点,一般是给出头结点的引用,直接循环 k–,直到 k = 1 结束,否则当前节点移动至后一位。代码实现也很简单:
public class LastKNode {
public static void main(String[] args) {
// 构建链表 1 --> 2 --> 3 --> 4
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
head.next.next.next = new ListNode(4);
// 查找第三个节点
ListNode kNode = findKNode(head, 3);
System.out.println(kNode.val);
}
public static ListNode findKNode(ListNode head, int k) {
while (k > 1) {
if (head != null) {
head = head.next;
}
k--;
}
return head;
}
}
那假设现在要求改成获取倒数第 k 个元素,该如何处理?
首先想到的方法,先用一个指针,从头到尾走完,并且边走边计数,可以获得链表的长度 n。然后再使用一个指针又从头开始,走到 n-k+1 的位置,就是倒数第 k 个元素。但是这样就需要遍历两次,并不优雅。
class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
public class LastKNode {
public static void main(String[] args) {
// 构建链表 1 --> 2 --> 3 --> 4
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
head.next.next.next = new ListNode(4);
// 查找倒数第三个节点:2
LastKNode lastKNode = new LastKNode();
ListNode kNode = lastKNode.findLastKNode(head, 3);
System.out.println(kNode.val);
}
public ListNode findLastKNode(ListNode head, int k) {
// 非法判断
if (head == null || k <= 0) {
return null;
}
ListNode first = head;
// 第一个指针先走 k 步
while (k != 0) {
if (first == null) {
return null;
}
first = first.next;
k--;
}
// 两个指针一起移动
while (first != null) {
head = head.next;
first = first.next;
}
return head;
}
}
判断一个链表是否有环
最直接的做法就是借助 HashSet,遍历的时候,判断 HashSet 是否存在该节点,如果不存在该节点,则往 HashSet 中添加该节点,并且指针后移。如果存在该节点,说明有环,则直接返回 true,如果遍历到 null 节点,说明遍历结束,没有找到相同节点,没有环的存在,返回 false。
import java.util.HashSet;
public class CycleLink {
public static void main(String[] args) {
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
head.next.next.next = new ListNode(4);
head.next.next.next.next = head.next;
System.out.println(isContainCycleLink(head));
}
public static boolean isContainCycleLink(ListNode head) {
HashSet<ListNode> sets = new HashSet<>();
while (head != null) {
if (sets.contains(head)) {
return true;
}
sets.add(head);
head = head.next;
}
return false;
}
}
那如果我们不借助额外的空间,怎么做呢?上一道题,我们提到快慢指针,这一道题,同样可以使用该方法,但是具体需要怎么执行呢?由于链表是有环的,因此,不可能像之前一样让一个指针走到尾部。
如果我们让一个快指针每次走两步,慢指针每次走一步,如果快慢两个指针能够相遇的话,说明快指针走过环并且已经从后面追上了慢指针,那就可以证明环的存在了。如果没有环,那么快指针会直接走到链表的尾部,到达 null 节点,此时链表肯定不存在环的。
public class CycleLink {
public static void main(String[] args) {
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
head.next.next.next = new ListNode(4);
head.next.next.next.next = head.next;
System.out.println(isContainCycleLink(head));
}
public static boolean isContainCycleLink(ListNode head) {
if (head == null || head.next == null) {
return false;
}
ListNode slow = head;
ListNode fast = head.next;
while (slow != fast) {
if (fast == null || fast.next == null) {
return false;
}
slow = slow.next;
fast = fast.next.next;
}
return true;
}
}
找出链表中环的入口和计算环的大小
class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
public class CycleLink {
public static void main(String[] args) {
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
head.next.next.next = new ListNode(4);
head.next.next.next.next = head.next;
System.out.println(findCycleStartNode(head).val);
}
public static ListNode findCycleStartNode(ListNode head) {
if (head == null || head.next == null) {
return null;
}
// 快指针
ListNode fast = head;
// 慢指针
ListNode slow = head;
// 两个指针同时移动
while (fast != null && fast.next != null) {
// 快指针每次移动两步
fast = fast.next.next;
// 慢指针每次移动一步
slow = slow.next;
if (fast == slow) {
break;
}
}
// 判空可以避免没有环的情况,没有环则不会相遇
if (fast == null || slow != fast) {
return null;
}
//有环则找入口节点,把快指针放到起点
fast = head;
// 再次同时移动
while (fast != slow) {
// 两个指针都是每次移动一次
fast = fast.next;
slow = slow.next;
}
// 相遇的地方就是环的起点
return fast;
}
}
找到两个链表的第一个共同节点
比较直接的做法,借助额外的空间 HashSet,遍历第一个链表的时候,将所有的节点,添加到 HashSet 中,遍历第二个链表的时候直接判断是否包含即可,属于空间换时间的做法,时间和空间复杂度都是 O(n) 级别。
import java.util.HashSet;
import java.util.Set;
class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
public class CommonNode {
public static void main(String[] args) {
// 链表 1
ListNode p1 = new ListNode(1);
p1.next = new ListNode(2);
p1.next.next = new ListNode(3);
// 链表 2
ListNode p2 = new ListNode(4);
p2.next = new ListNode(5);
// 相同节点
ListNode common = new ListNode(6);
common.next = new ListNode(7);
p1.next.next.next = common;
p2.next.next = common;
CommonNode commonNode = new CommonNode();
ListNode listNode = commonNode.findFirstCommonNode(p1, p2);
System.out.println(listNode.val);
}
public ListNode findFirstCommonNode(ListNode pHead1, ListNode pHead2) {
//创建集合 set
Set<ListNode> set = new HashSet<>();
// 把链表 1 的节点添加进去
while (pHead1 != null) {
set.add(pHead1);
pHead1 = pHead1.next;
}
// 遍历链表 2 的节点
while (pHead2 != null) {
// 如果包含就返回
if (set.contains(pHead2)) return pHead2;
pHead2 = pHead2.next;
}
return null;
}
}
可以每个链表遍历一遍,计算链表里面的元素个数,计算出两个链表的差值,然后可以使用两个指针,分别指向链表的头部。好让长链表的指针先走 k = 1 步,如此相当于两个指针后面需要走的链表长度相等了。
class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
public class CommonNode {
public static void main(String[] args) {
// 链表 1
ListNode p1 = new ListNode(1);
p1.next = new ListNode(2);
p1.next.next = new ListNode(3);
// 链表 2
ListNode p2 = new ListNode(4);
p2.next = new ListNode(5);
// 相同节点
ListNode common = new ListNode(6);
common.next = new ListNode(7);
p1.next.next.next = common;
p2.next.next = common;
CommonNode commonNode = new CommonNode();
ListNode listNode = commonNode.findFirstCommonNode(p1, p2);
System.out.println(listNode.val);
}
public ListNode findFirstCommonNode(ListNode pHead1, ListNode pHead2) {
// 只要有一个为空,就不存在共同节点
if (pHead1 == null || pHead2 == null) {
return null;
}
// 计算链表 1 中的节点个数
int numOfListNode1 = 0;
ListNode head1 = pHead1;
while (head1 != null) {
numOfListNode1++;
head1 = head1.next;
}
// 计算链表 2 中节点个数
int numOfListNode2 = 0;
ListNode head2 = pHead2;
while (head2 != null) {
numOfListNode2++;
head2 = head2.next;
}
// 比较两个链表的长度
int step = numOfListNode1 - numOfListNode2;
if (step > 0) {
// 链表 1 更长,链表 1 移动
while (step != 0) {
pHead1 = pHead1.next;
step--;
}
} else {
// 链表 2 更长,链表 2 移动
while (step != 0) {
pHead2 = pHead2.next;
step++;
}
}
// 循环遍历后面的节点,相等则返回
while (pHead1 != null && pHead2 != null) {
if (pHead1 == pHead2) {
return pHead1;
} else {
pHead1 = pHead1.next;
pHead2 = pHead2.next;
}
}
return null;
}
}
如何翻转一个链表
class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
public class ReverseList {
public static void main(String[] args) {
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
head.next.next.next = new ListNode(4);
// 打印
printList(head);
// 翻转链表
ListNode rHead = reverse(head);
// 打印
printList(rHead);
}
public static ListNode reverse(ListNode head) {
if (head == null) {
// 判空
return head;
} else {
// 新建空节点
ListNode first = null;
while (head != null) {
// 保存下一个节点
ListNode temp = head.next;
// 将 head 的下一个节点指针修改为指向左边
head.next = first;
// 修改左边的链表的头节点
first = head;
// 修改需要翻转的头结点指针
head = temp;
}
return first;
}
}
// 打印链表
public static void printList(ListNode head) {
ListNode p = head;
while (p != null) {
System.out.print(p.val + " --> ");
// 移动到下一个元素
p = p.next;
}
System.out.println(" null");
}
}
如何合并有序链表
创建一个 -1 节点的新链表,然后两个链表都从头开始遍历,循环直到一个链表遍历到最后,在这个过程中,哪一个链表的节点小,就加入新的链表后面,移动到后面一个节点,接着比较。
之后遍历两个链表剩下的元素,这些元素肯定比另一个链表的所有元素都大或者相等,直接加入新的链表后面即可。
class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
public class MergeList {
public static void main(String[] args) {
// 1-->3-->4
ListNode head1 = new ListNode(1);
head1.next = new ListNode(3);
head1.next.next = new ListNode(4);
// 2-->6——>7
ListNode head2 = new ListNode(2);
head2.next = new ListNode(6);
head2.next.next = new ListNode(7);
ListNode head = merge(head1,head2);
printList(head);
}
public static ListNode merge(ListNode list1, ListNode list2) {
// 判空
if (list1 == null) {
return list2;
} else if (list2 == null) {
return list1;
} else {
// 创建-1头节点
ListNode head = new ListNode(-1);
ListNode first = head;
// 只要不为空,则比较
while (list1 != null && list2 != null) {
// list1的节点更小
if (list1.val < list2.val) {
first.next = new ListNode(list1.val);
list1 = list1.next;
} else {
// list2的节点更小
first.next = new ListNode(list2.val);
list2 = list2.next;
}
// 新链表指针后移
first = first.next;
}
// 如果list1有剩余元素全部遍历加入
while (list1 != null) {
first.next = new ListNode(list1.val);
list1 = list1.next;
first = first.next;
}
// 如果list2有剩余元素全部遍历加入
while (list2 != null) {
first.next = new ListNode(list2.val);
list2 = list2.next;
first = first.next;
}
return head.next;
}
}
// 打印链表
public static void printList(ListNode head) {
ListNode p = head;
while (p != null) {
System.out.print(p.val + " --> ");
// 移动到下一个元素
p = p.next;
}
System.out.println(" null");
}
}
蓝桥杯真题:幸运数
问题描述
幸运数是波兰数学家乌拉姆命名的。它采用与生成素数类似的“筛法”生成。
首先从 1 开始写出自然数 1,2,3,4,5,6,…,1 就是第一个幸运数。
我们从 2 这个数开始。把所有序号能被 2 整除的项删除,变为:1 _ 3 _ 5 _ 7 _ 9 …。
把它们缩紧,重新记序为:1 3 5 7 9 … 。这时,3 为第 2 个幸运数,然后把所有能被 3 整除的序号位置的数删去。注意!!是序号位置,不是那个数本身能否被 3 整除。 删除的应该是 5,11,17,…
此时 7 为第 3 个幸运数,然后再删去序号位置能被 7 整除的(19,39,…)。
最后剩下的序列类似:
1, 3, 7, 9, 13, 15, 21, 25, 31, 33, 37, 43, 49, 51, 63, 67, 69, 73, 75, 79, …
package com.ty.test01;
import java.util.LinkedList;
import java.util.Scanner;
public class LuckyNum {
public static void main(String[] args) {
Scanner scan=new Scanner(System.in);
int m = scan.nextInt();
int n=scan.nextInt();
scan.close();
int result = findCountOfLuck(1, 20);
System.out.println(result);
}
public static int findCountOfLuck(int m,int n){
LinkedList<Integer> list=new LinkedList<>();
int index=1;
for(int i=1;i<n;i+=2){
list.add(i);
}
while(index<list.size()){
int size=list.size();
int num=(int)list.get(index);
int numOfRemove=0;
for (int i = 0; i <size; i++) {
if ((i+1)%num==0) {
list.remove(i-numOfRemove);
numOfRemove++;
}
}
index++;
}
int count=0;
while(!list.isEmpty()){
int popNUm = list.pop();
if(popNUm>m&&popNUm<n){
count++;
}
}
return count;
}
}