数组和链表的常用算法

有趣的约瑟夫环问题

问题描述

著名的约瑟夫问题:编号为 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;
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值