牛客网算法小结(2)

一:树

1. 折纸问题

【题目】 请把一段纸条竖着放在桌子上,然后从纸条的下边向上方对折1次,压出折痕后展开。此时 折痕是凹下去的,即折痕突起的方向指向纸条的背面。如果从纸条的下边向上方连续对折2 次,压出折痕后展开,此时有三条折痕,从上到下依次是下折痕、下折痕和上折痕。给定一 个输入参数N,代表纸条都从下边向上方连续对折N次,请从上到下打印所有折痕的方向。 例如:N=1时,打印: down N=2时,打印: down down up

/**
 * 折纸问题,就是一棵左结点为down,右结点为up的二叉树
 */
public class Paper {
    static class TreeNode{
        private String val;
        private TreeNode left;
        private TreeNode right;

        public TreeNode(String val) {
            this.val = val;
        }

        @Override
        public String toString() {
            return "TreeNode{" +
                    "val=" + val +
                    ", left=" + left +
                    ", right=" + right +
                    '}';
        }
    }
    public static void print(int N){
        if (N < 1)
            return;
        TreeNode root = new TreeNode("down");
        build(root,N);
        print(root);
    }

    private static void print(TreeNode node){
        if (node.left != null)
            print(node.left);
        System.out.println(node.val);
        if (node.right != null)
            print(node.right);
    }

    private static void  build(TreeNode node,int N){
        if (N <= 1)
            return;
        node.left = new TreeNode("down");
        node.right = new TreeNode("up");
        build(node.left,N-1);
        build(node.right,N-1);
    }


    public static void main(String[] args) {
        print(3);
    }
}

2. 二叉树先序、中序,后序遍历的非递归实现

    //递归前序遍历
    public static void pre1(TreeNode node){
        if (node == null)
            return;
        System.out.printf(node.val+" ");
        pre1(node.left);
        pre1(node.right);
    }

    //非递归前序遍历
    public static void pre2(TreeNode node){
        if (node == null)
            return;
        Stack<TreeNode> stack = new Stack<>();
        while (!stack.isEmpty() || node != null){
            //把所有左结点加入到栈
            if (node != null){
                System.out.printf(node.val+" ");
                stack.push(node);
                node = node.left;
            }
            //左结点都为空,把上一个结点的右结点入栈
            else
                node = stack.pop().right;
        }
    }

    //递归中序遍历
    public static void in1(TreeNode node){
        if (node == null)
            return;
        in1(node.left);
        System.out.printf(node.val+" ");
        in1(node.right);
    }

    //非递归中序遍历
    public static void in2(TreeNode node){
        if (node == null)
            return;
        Stack<TreeNode> stack = new Stack<>();
        while (!stack.isEmpty() || node != null){
            //把所有左结点加入到栈
            if (node != null){
                stack.push(node);
                node = node.left;
            }
            //左结点都为空,把上一个结点的右结点入栈
            else
            {
                node = stack.pop();
                System.out.printf(node.val+" ");
                node = node.right;
            }
        }
    }

    //递归后序遍历
    public static void post1(TreeNode node){
        if (node == null)
            return;
        post1(node.left);
        post1(node.right);
        System.out.printf(node.val+" ");
    }

    //非递归后序遍历
    public static void post2(TreeNode node){
        if (node == null)
            return;
        ArrayList<TreeNode> nodes = new ArrayList<>();
        Stack<TreeNode> stack = new Stack<>();
        while (!stack.isEmpty() || node != null){
            //把所有右结点加入到栈
            if (node != null){
                nodes.add(node);
                stack.push(node);
                node = node.right;
            }
            //右结点都为空,把上一个结点的左结点入栈
            else
                node = stack.pop().left;
        }
        Collections.reverse(nodes);
        for (TreeNode node1:
             nodes) {
            System.out.printf(node1.val+" ");
        }
    }

3. morris遍历

特点:

  • 时间复杂度为O(N),空间复杂度为O(1),并且遍历过程不会改变树的形状
  • 对于有左子树的结点,访问两次,对于没有左子树的结点,只访问一次
  • 大体上沿着当前结点、左、当前结点、右方向移动

要求:

  • cur无左子树,向右移动(左边没路,那我直接向右走)
  • cur有左子树,找到左子树的最右结点mostRight
    • mostRight的右孩子为空:mostRight = cur,cur向左移动(标记自己来过一次,去左边看看)
    • mostRight的右孩子为cur:mostRight = null,cur向右移动(看到自己的标记了,左边我走过了,直接去右边)
/**
 * morris遍历
 */
public class MorrisTravel {
    static class TreeNode {
        private int val;
        private TreeNode left;
        private TreeNode right;

        public TreeNode(int val) {
            this.val = val;
        }

        @Override
        public String toString() {
            return "TreeNode{" +
                    "val=" + val +
                    ", left=" + left +
                    ", right=" + right +
                    '}';
        }
    }

    public static void inTravel(TreeNode root) {
        if (root == null)
            return;
        TreeNode cur = root;
        TreeNode next = null;
        while (cur != null) {
            next = cur.left;
           //有左子树
            if (next != null) {
                while (next.right != cur && next.right != null)
                    next = next.right;
                //第一次访问,改一下指针,向左走(中断)
                if (next.right == null) {
                    next.right = cur;
                    cur = cur.left;
                    continue;
                }
                //第二次访问,修复指针,向右走↓
                else
                    next.right = null;
            }

            //左子树遍历完成(左-根-右)
            System.out.printf(cur.val + " ");
            cur = cur.right;
        }
    }

    public static void preTravel(TreeNode root) {
        if (root == null)
            return;
        TreeNode cur = root;
        TreeNode next = null;
        while (cur != null) {
            next = cur.left;
           //有左子树
            if (next != null) {
                while (next.right != cur && next.right != null)
                    next = next.right;
                //第一次访问,改一下指针,向左走(中断)
                if (next.right == null) {
                     next.right = cur;
                    //(根-左)
                    System.out.printf(cur.val + " ");
                    cur = cur.left;
                    continue;
                }
                //第二次访问,修复指针,向右走↓
                else
                    next.right = null;
            }
            //左子树遍历完成
            else
                //(根-右)
                System.out.printf(cur.val + " ");
            cur = cur.right;

        }
    }

    public static void postTravel(TreeNode root) {
        if (root == null)
            return;
        TreeNode cur = root;
        TreeNode next = null;
        while (cur != null) {
            next = cur.left;
           //有左子树
            if (next != null) {
                while (next.right != cur && next.right != null)
                    next = next.right;
                //第一次访问,改一下指针,向左走(中断)
                if (next.right == null) {
                    next.right = cur;
                    cur = cur.left;
                    continue;
                }
                //第二次访问,修复指针,向右走↓
                else
                {
                    next.right = null;
                    //已经走完左边和右边了(左-右-根)
                    //打印左子树的倒置右结点
                    reverse(cur.left);
                }
            }
            cur = cur.right;

        }
        //打印根节点的倒置右结点
        //左边和右边都走完了
        reverse(root);
    }

    private static void reverse(TreeNode node){
        if (node == null)
            return;
        TreeNode next = node.right;
        TreeNode temp = null;
        node.right = null;
        while (next != null){
            temp = next.right;
            next.right = node;
            node = next;
            next = temp;
        }
        next = node;
        while (next != null){
            System.out.printf(next.val + " ");
            next = next.right;
        }
        next = node.right;
        node.right = null;
        while (next != null){
            temp = next.right;
            next.right = node;
            node = next;
            next = temp;
        }
    }
}

4. 在二叉树中找到一个节点的后继节点

【题目】 现在有一种新的二叉树节点类型如下:

public class Node { 
	public int value; 
	public Node left; 
	public Node right; 
	public Node parent;
	public Node(int data) { this.value = data; }
}

该结构比普通二叉树节点结构多了一个指向父节点的parent指针。假设有一 棵Node类型的节点组成的二叉树,树中每个节点的parent指针都正确地指向 自己的父节点,头节点的parent指向null。只给一个在二叉树中的某个节点 node,请实现返回node的后继节点的函数。在二叉树的中序遍历的序列中, node的下一个节点叫作node的后继节点。

    public static Node nextNode(Node node){
        if (node == null)
            return null;
        //中序遍历是左-中-右
        //如果有右结点,那么就在下一层找,找到右孩子的最左边的结点
        if (node.right != null){
            node = node.right;
            while (node != null && node.left != null) {
                node = node.left;
            }
            return node;
        }
        //如果没有右结点,那么就只能往上一层查找,找到一个作为左结点的父结点
        else
            while (node.parent != null && node != node.parent.left)
                node = node.parent;
            return node.parent;
    }

5. 何为前缀树? 如何生成前缀树?

/**
 * 前缀树
 */
public class PrefixTree {
    static class PrefixNode {
        //经过结点的单词个数
        private int pass;
        //单词长度
        private int end;
        //下一个结点
        private HashMap<Integer, PrefixNode> nexts;

        public PrefixNode() {
            pass = 0;
            end = 0;
            nexts = new HashMap<>();
        }

        @Override
        public String toString() {
            return "PrefixNode{" +
                    "pass=" + pass +
                    ", end=" + end +
                    ", nexts=" + nexts +
                    '}';
        }
    }
    private PrefixNode root;

    public PrefixTree() {
        root = new PrefixNode();
    }

    public void insert(String word){
        if (word == null || word.length() == 0)
            return;
        PrefixNode node = root;
        PrefixNode next;
        for (int i = 0; i < word.length(); i++) {
            char s = word.charAt(i);
            next = node.nexts.get((int)s);
            if (next == null)
                 next = new PrefixNode();
            node.nexts.put((int)s,next);
            next.pass++;
            node = next;
        }
        node.end++;
    }

    public void remove(String word){
        if (prefixSize(word) != 0)
        {
            PrefixNode node = root;
            PrefixNode next;
            for (int i = 0; i < word.length(); i++) {
                char s = word.charAt(i);
                next = node.nexts.get((int)s);
                if (--next.pass == 0){
                    node.nexts.put((int)s,null);
                    return;
                }
                node = next;
            }
            node.end--;
        }
    }

    public int prefixSize(String prefix){
        PrefixNode node = root;
        if (prefix == null || prefix.length() == 0)
            return 0;
        int index = 0;
        while (index < prefix.length() && node != null)
        {
            char c = prefix.charAt(index++);
            node = node.nexts.get((int)c);
        }
        if (node == null)
            return 0;
        return node.pass;
    }

    public int count(String word){
        PrefixNode node = root;
        if (word == null || word.length() == 0)
            return 0;
        int index = 0;
        while (index < word.length() && node != null)
        {
            char c = word.charAt(index++);
            node = node.nexts.get((int)c);
        }
        if (node == null)
            return 0;
        return node.end;
    }

    @Override
    public String toString() {
        return "PrefixTree{" +
                "root=" + root +
                '}';
    }
}

打印数组

一个字符串类型的数组arr1,另一个字符串类型的数组arr2。arr2中有哪些字符,是arr1中出现的?请打印

arr2中有哪些字符,是作为arr1中某个字符串前缀出现的?请打印

    public static void main(String[] args) {
        PrefixTree tree = new PrefixTree();
        String[] arr1 = {"abc","abd","bcd"};
        String[] arr2 = {"abd","cc","ab"};

        //打印数组
        for (int i = 0; i < arr1.length; i++) {
            tree.insert(arr1[i]);
        }
        System.out.println("出现的字符:");
        for (int i = 0; i < arr2.length; i++) {
            if (tree.count(arr2[i]) > 0)
                System.out.println(arr2[i]);
        }
        System.out.println("作为前缀的字符:");
        for (int i = 0; i < arr2.length; i++) {
            if (tree.prefixSize(arr2[i]) > 0)
                System.out.println(arr2[i]);
        }

    }

二:布隆过滤器

作用:黑名单是一个大文件,用于过滤某个信息。如果信息属于黑名单,则会被过滤;如果信息不属于黑名单,也可能会被失误过滤。

结构:大数组,数组元素是一个bit位

定位信息:

  1. 定位数组的索引:X/位数
  2. 定位该索引的位数:X%位数

描黑:

  1. 准备K个哈希函数
  2. 将信息分别通过哈希函数,得到K个哈希值,把相应的K个位数描黑

检测:

  1. 将信息分别通过哈希函数,得到K个哈希值
  2. 如果相应的位都是黑的,则拦截;如果有一个位不是黑的,则放过

特点:

  • 与单样本的大小无关
  • 与样本数量和失误率有关

三:一致性哈希

原来的结构

  1. 客户端发出一个key,请求一个value
  2. 服务端通过对key的哈希函数求出哈希值,模服务器个数,确定提供服务的服务器X
  3. 由服务器X提供服务

问题:

  • 服务器的增加和删除,数据迁移的代价高
  • 改动代码

改进的结构

  1. 服务端通过对每个服务器的ip求出哈希值,连成环
  2. 客户端发出一个key,请求一个value
  3. 服务端通过对key的哈希函数求出哈希值,打到环上,确定环上顺时针方向的下一个服务器X(通过二分法确定)
  4. 由服务器X提供服务

特点:

  • 添加一个服务器X1后,找到顺时针方向的下一个服务器X2,请他把X1-X0的数据传给X1
  • 初级服务器个数少时,负载不均衡
  • 添加服务器后,负载不均衡

再改进的结构

  1. 每个服务器分配一定量的虚拟节点,放在路由表
  2. 服务端通过对每个服务器的每个虚拟节点求出哈希值,连成环

四:哈希表

1. 并查集

  1. 查找两个元素是否属于同一个集合
  2. 把两个集合合并为一个大集合
/**
 * 并查集
 */
public class UnionSet {
    static class Node{
        private int val;

        public Node(int val) {
            this.val = val;
        }
    }
    //记录每个节点的代表节点
    HashMap<Node,Node> fathers;
    //记录每个代表节点的集合个数
    HashMap<Node,Integer> sizes;

    public UnionSet(List<Node> nodes){
        fathers = new HashMap<>();
        sizes = new HashMap<>();
        build(nodes);
    }

    //初始化
    private void build(List<Node> nodes){
        fathers.clear();
        sizes.clear();
        for (Node node :
                nodes) {
            fathers.put(node, node);
            sizes.put(node,1);
        }
    }

    //查找代表节点
    private Node findPresent(Node node){
        Node present = fathers.get(node);
        //找出代表节点为本身的节点
        if (present != node) {
            present = findPresent(present);
        }
        //找到之后,把其他节点都更新代表节点,加快查询速度
        fathers.put(node,present);
        return present;
    }

    //判断是否是同一个集合
    public boolean isSame(Node a,Node b){
        if (a == null || b == null)
            return false;
        return findPresent(a) == findPresent(b);
    }

    //合并集合
    public void union(Node a,Node b){
        if (a == null || b == null || isSame(a,b))
            return;
        int size1 = sizes.get(a);
        int size2 = sizes.get(b);
        if (size1 > size2) {
            fathers.put(b, a);
            sizes.put(a,size1+size2);
        }
        else {
            fathers.put(a, b);
            sizes.put(b,size1+size2);
        }
    }
}

2. 岛问题

一个矩阵中只有0和1两种值, 每个位置都可以和自己的上、 下、 左、 右四个位置相连

如果有一片1连在一起, 这个部分叫做一个岛, 求一个矩阵中有多少个岛?

举例:
0 0 1 0 1 0
1 1 1 0 1 0
1 0 0 1 0 0
0 0 0 0 0 0
这个矩阵中有三个岛

/**
 * 岛问题
 */
public class Island {
    static int sum = 0;

	//遍历岛
    public static int getIsland(int[][] matrix){
        if (matrix == null || matrix[0] == null)
            return 0;
        int N = matrix.length;
        int M  = matrix[0].length;
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < M; j++) {
                if (matrix[i][j] == 1){
                	//找到有病毒的岛,开始感染
                    sum++;
                    getIsland(matrix,i,j,N,M);
                }
            }
        }
        return sum;
    }
    //不断感染,直到没有病菌
    public static void getIsland(int[][] matrix,int x,int y,int N,int M){
        if (x < 0 || y < 0 || x >= N || y >= M || matrix[x][y] != 1)
            return;
        matrix[x][y] = 2;
        getIsland(matrix,x+1,y,N,M);
        getIsland(matrix,x-1,y,N,M);
        getIsland(matrix,x,y+1,N,M);
        getIsland(matrix,x,y-1,N,M);

    }
    public static void main(String[] args) {
        int[][] matrix = {{0,0,1,0,1,0},{1,1,1,0,1,0},{1,0,0,1,0,0},{0,0,0,0,0,0}};
        System.out.println(getIsland(matrix));
    }
}

2. 随时找到数据流的中位数

【题目】 有一个源源不断地吐出整数的数据流,假设你有足够的空间来 保存吐出的数。请设计一个名叫MedianHolder的结构, MedianHolder可以随时取得之前吐出所有数的中位数。

【要求】

  1. 如果MedianHolder已经保存了吐出的N个数,那么任意时刻 将一个新数加入到MedianHolder的过程,其时间复杂度是 O(logN)。
  2. 取得已经吐出的N个数整体的中位数的过程,时间复杂度为 O(1)
/**
 * 保存中位数
 */
public class MedianHolder {
    class MyComparator implements Comparator<Integer> {
        @Override
        public int compare(Integer o1, Integer o2) {
            if (o1 > o2)
                return -1;
            else if(o1 < o2)
                return 1;
            else
                return 0;
        }
    }
    //最小堆和最大堆
    PriorityQueue<Integer> min = new PriorityQueue<>();
    PriorityQueue<Integer> max = new PriorityQueue<>(new MyComparator());

    //最小堆只添加比自己大的数字,这样才不用调整
    //最大堆只添加比自己小的数字,或者比自己大又比其他小的数字
    //维持两个堆的平衡
    public void add(int num){
        if (max.isEmpty() || num <= max.peek())
            max.add(num);
        else if (min.isEmpty() || num >= min.peek())
            min.add(num);
        else
            max.add(num);
        modify();
    }

    //保证两个堆最多相差一个
    private void modify(){
        if (min.size() - max.size() > 1)
            max.add(min.poll());
        if (max.size() - min.size() > 1)
            min.add(max.poll());
    }

    //奇数个数字,则在多一个数量的堆里面
    //偶数个数字,取中间值
    public int get(){
        if (max.isEmpty() && min.isEmpty())
            return Integer.MIN_VALUE;
        if (min.isEmpty())
            return max.peek();
        if (max.isEmpty())
            return min.peek();
        int minSize = min.size();
        int maxSize = max.size();
        if ((maxSize+minSize) % 2 == 0)
            return (max.peek() + min.peek())/2;
        else if (maxSize > minSize)
            return max.peek();
        else
            return min.peek();
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_36904568/article/details/94056371