123

题2: 实现单例类 - done

题3: 二维数据查找 - done
题14: 调整数组顺序,使奇数在前偶数在后 - done
题20: 顺时针打印数组- done
题29: 数组中出现次数超过一半的数字 - done
题30: 最小的K个数 - done
题31: 连续子数组和的最大值 - done
题41: 有序数组查找“和为s”的两个数 - done
题51: 数组中重复的数字 - done

分析规律+快排定位的方法


题5: 单链表反向打印 - done
题13: 在O(1)时间删除单链表节点 - done
题15: 单链表查找倒数第k个节点 - done
题16: 单链表反转 - done
题17: 合并两个升序单链表 - done
题37: 两个单链表的第一个公共节点 - done

递归+多个指针


题7: 两个栈实现队列 - done
题7.1: 两个队列实现栈 - done

题9: 斐波那契数列 - done
题10: 二进制中1的个数 - done
题40: 数组中只出现一次的数 - done
题45: 约瑟夫问题 - done
题47: 不用加减乘除实现加法 - done
题47.1: 不使用新的变量,交换两个变量的值 - done

总结出数学公式+位运算+递归


题35: 第一个只出现一次的字符 - done

题58: 二叉树的下一个节点
题58: 判断一个二叉树是否对称
题60: 二叉树打印成多行
题61: 之字形打印二叉树
题62: 二叉搜索树的第k个节点
题62.1: 二叉树中序遍历

题2: 实现单例类

单例模式:

  1. 单例类确保该类只有唯一一个实例
  2. 单例类自己创建这一实例
  3. 单例类给所有其他对象提供这一实例

最简单的实现方式

// 饿汉式单例
public class Single {
    
    private static Single single = new Single();

    private Single() {

    }

    public static Single getSingle() {
        return single;
    }
}

解释

  1. static变量也叫静态变量,静态变量被所有的对象所共享,在内存中只有一个副本。饿汉式单例在类加载初始化时就创建好一个静态的对象供外部使用,除非系统重启,这个对象不会改变,所以本身就是线程安全的。
  2. 构造方法限定为private避免了类在外部被实例化
  3. Single类的唯一实例只能通过getSingle()方法访问

缺点
无论这个类是否被使用,都会被实例化,浪费内存

较好的实现方式

public class Single1 {
    // volatile保证不同线程对这个变量进行操作时的可见性
    // 即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的
    private volatile static Single1 single1 = null;

    private Single1() {

    }

    public static Single1 getSingle1() {
        // 如果每次执行getSingle1都加锁,很影响性能
        // 因此先判断,如果null,加锁初始化,如果非null,直接返回
        if (single1 == null) {
            synchronized (Single1.class) { // 锁,保证线程安全
                if (single1 == null) {
                    single1 = new Single1();
                }
            }
        }
        return single1;
    }
}

https://www.cnblogs.com/limingluzhu/p/5156659.html


题3: 二维数组查找
题14: 调整数组顺序,使奇数在前偶数在后
    // 调整数组顺序,使奇数在前偶数在后
    private static void handleArr(int[] arr) {
        if (arr == null) {
            return;
        }

        int start = 0;
        int end = arr.length - 1;
        int tem;
        while (start != end) {
            if ((arr[start] & 1) == 1) { // 奇数
                start++;
            }else { // 偶数
                tem = arr[start];
                arr[start] = arr[end];
                arr[end] = tem;
                end--;
            }
        }
    }
题20: 顺时针打印数组

思路:确定圈数,针对圈中每条边分别打印,注意边界

    // 顺时针打印数组
    private static void circlePrintArr(int[][] arr) {
        if (arr == null || arr.length <= 0 || arr[0].length <= 0) {
            return;
        }
        int row = arr.length; // 总行数
        int col = arr[0].length; // 总列数
        int totalCir = (Math.min(row, col) + 1) >> 1; // 一共打印的圈数
        for (int start = 0; start < totalCir; start++) {
            printCircle(arr, row, col, start);
        }
    }

    // row总行数,col总列数,start当前圈数
    private static void printCircle(int[][] arr, int row, int col, int start) {
        // 边界:左上角arr[start][start]
        // 边界:右下角arr[endX][endY]
        int endX = row - start - 1;
        int endY = col - start - 1;

        // 打印上边
        for (int i = start; i <= endY; i++) {
            System.out.print(arr[start][i] + " ");
        }

        // 打印右边
        for (int i = start + 1; i <= endX; i++) {
            System.out.print(arr[i][endY] + " ");
        }

        // 打印下边
        if (endX > start) {
            for (int i = endY - 1; i >= start; i--) {
                System.out.print(arr[endX][i] + " ");
            }
        }

        // 打印左边
        if (endY > start) {
            for (int i = endX - 1; i >= start + 1; i--) {
                System.out.print(arr[i][start] + " ");
            }
        }
    }
题29: 数组中出现次数超过一半的数字

描述:数组中又一个数字出现的次数,大于数组长度的一半,找出这个数
思路一:

  1. 快排定位方法
  2. 定位一次,如果定位不在中间,则重长的子数组在进行定位,直到定位在中间
    思路二:
    时间复杂度O(n)
  3. 维护两个数,一个存值,一个存值出现的次数
  4. 遍历数组,相同-加次数,不同-减次数,次数为0-更新值
    // 数组中出现次数超过一半的数字
    private static Integer moreThanHalfNum(int[] arr) {
        if (arr == null || arr.length <= 1) {
            return null;
        }

        int key = arr[0];
        int cnt = 1;
        for (int i = 1; i < arr.length; i++) {
            if (cnt == 0) {
                key = arr[i];
                cnt++;
            }else if (arr[i] == key) {
                cnt++;
            }else {
                cnt--;
            }
        }
        return key;
    }
题30: 最小的K个数

思路:利用快排的定位方法

  1. 定位,判断左边个数如果不为k-1,继续定位,直到左边个数为k-1
    // 最小的k个数
    private static void leastKNumbers(int[] arr, int k) {
        if (arr == null || k <= 0 || arr.length < k) {
            return;
        }

        int start = 0;
        int end = arr.length - 1;
        int pos = Sort.position(arr, start, end);
        while (pos != k - 1) {
            if (pos > k - 1) {
                end = pos - 1;
            }else {
                start = pos + 1;
            }
            pos = Sort.position(arr, start, end);
        }

        for (int i = 0; i < k; i++) {
            System.out.print(arr[i] + " ");
        }
    }
题31: 连续子数组和的最大值

思路:时间复杂度O(n)

  1. 用f(i)来表示,所有以i结尾的子数组中,子数组和的最大值
  2. 那么题目答案转换为,求max(f(i))
  3. 如果f(i-1)小于0,那么f(i) = arr[i],如果f(i-1)大于0,那么f(i) = f(i-1) + arr[i]
    // 连续子数组,和的最大值
    private static Integer findMax(int[] arr) {
        if (arr == null || arr.length == 0) {
            return null;
        }

        int sum = 0; // f(i)
        int arrMax = 0x80000000; // max(f(i))
        for (int i = 0; i <= arr.length - 1; i++) {
            if (sum <= 0) {
                sum = arr[i];
            }else {
                sum = sum + arr[i];
            }

            if (sum > arrMax) {
                arrMax = sum;
            }
        }
        return arrMax;
    }
题41: 有序数组查找“和为s”的两个数

思路:头尾两个指针

    // 有序数组查找“和为s”的两个数
    private static boolean findNumWithSum(int[] arr, int sum) {
        if (arr == null || arr.length < 2) {
            return false;
        }

        int start = 0;
        int end = arr.length - 1;
        int curSum;
        while (end > start) {
            curSum = arr[start] + arr[end];
            if (curSum == sum) {
                System.out.println(arr[start]);
                System.out.println(arr[end]);
                return true;
            }else if (curSum > sum) {
                end--;
            }else {
                start++;
            }
        }
        return false;
    }
题51: 数组中重复的数字

描述:长度为n的数组,没个数都在[0, n-1]内,求数组中是否有重复数字
思路:分析数组规律,如果数组中没有重复数字,那么下标为i的位置,值也为i

    // 数组中重复的数字
    private static boolean isRepeat(int[] arr) {
        if (arr == null || arr.length == 0) {
            return false;
        }

        int tem;
        for (int i = 0; i < arr.length; i++) {
            while (arr[i] != i) {
                if (arr[i] == arr[arr[i]]) {
                    System.out.println(arr[i]);
                    return true;
                }
                tem = arr[i];
                arr[i] = arr[tem];
                arr[tem] = tem;
            }
        }
        return false;
    }

题5: 单链表反向打印

提示:递归

    private static void reverseList(Node node) {
        if (node == null) {
            return;
        }
        if (node.next == null) {
            System.out.println(node.num);
            return;
        }
        reverseList(node.next);
        System.out.println(node.num);
    }
题13: 在O(1)时间删除单链表节点

描述:假定待删除节点在给定链表内
思路:有O(1)的时间要求,不能遍历,可以用“待删除节点的next节点”的内容,覆盖“待删除节点”的内容。

比如

  • 给定链表:n1 -> n2 -> n3 -> n4 -> null
  • 删除节点:n2
    此时一定不能用n2 = n3,因为n2 = n3达到的效果如下图,n2原来指向b93,执行n2 = n3后,n2指向80c,而原链表并没有变化


    IMG_4117.JPG

    而应该用n3的内容覆盖n2的内容,然后删除n3

    // O(1)时间内删除单链表节点
    private static void delNode(Node head, Node delNode) {
        if (head == null || delNode == null) {
            return;
        }

        Node tem;
        // 待删除节点不是尾节点
        if (delNode.next != null) {
            tem = delNode.next;
            delNode.num = tem.num;
            delNode.next = tem.next;
        }else if (head != delNode){ // 待删除节点不是头节点,是尾节点
            tem = head;
            while (tem.next != delNode) {
                tem = tem.next;
            }
            tem.next = null;
            delNode = null;
        }else { // 链表长度1,删除头节点
            head = null;
            delNode = null;
        }
        tem = null;
    }

虽然有一种场景需要遍历n-1,但是平均时间复杂度仍然是O(1),不再解释

题15: 单链表查找倒数第k个节点

思路:先做条件判断,以免程序崩溃

  • 链表为空
  • k <= 0
  • k大于链表长度
    // 单链表倒数第k个节点
    private static Node findKTail(Node head, int k) {
        if (head == null || k <= 0) {
            return null;
        }

        Node fast = head;
        // 快慢指针,快指针先走k-1步
        for (int i = 0; i < k - 1; i++) {
            fast = fast.next;
            if (fast == null) { // k大于链表长度
                return null;
            }
        }

        Node slow = head;
        while (fast.next != null) {
            slow = slow.next;
            fast = fast.next;
        }
        return slow;
    }
题16: 单链表反转

思路:三个指针

    private Node singleListRoll(Node node) {
        if (node == null) {
            return null;
        }

        Node pre = null;
        Node cur = node;
        Node nex;
        while(cur != null) {
            nex = cur.next;
            cur.next = pre;
            pre = cur;
            cur = nex;
        }
        return pre;
    }
题17: 合并两个升序单链表

思路:用递归做

    // 合并两个升序链表
    private static Node merge1(Node h1, Node h2) {
        if (h1 == null) {
            return h2;
        }
        if (h2 == null) {
            return h1;
        }

        Node h = null;
        if (h1.num <= h2.num) {
            h = h1;
            h.next = merge1(h1.next, h2);
        }else {
            h = h2;
            h.next = merge1(h1, h2.next);
        }
        return h;
    }
题37: 两个单链表的第一个公共节点

思路:时间复杂度O(n)

  1. 先得到两个链表长度
  2. 长链表先走
  3. 两个链表一起走,第一个相同
    // 两个单链表第一个相同节点
    private static Node findFirstNode(Node h1, Node h2) {
        if (h1 == null || h2 == null) {
            return null;
        }
        int l1 = nodeLength(h1);
        int l2 = nodeLength(h2);
        Node longNode = h1;
        Node shorNode = h2;
        int dif = l1 - l2;

        if (l2 > l1) {
            longNode = h2;
            shorNode = h1;
            dif = l2 - l1;
        }

        for (int i = 0; i < dif; i++) {
            longNode = longNode.next;
        }

        while (longNode != null && shorNode != null) {
            if (longNode == shorNode) {
                break;
            }
            longNode = longNode.next;
            shorNode = shorNode.next;
        }
        return longNode;
    }

    private static int nodeLength(Node node) {
        int l = 0;
        Node n = node;
        while (n != null) {
            l++;
            n = n.next;
        }
        return l;
    }

题7: 两个栈实现队列

思路:

  1. 两个栈,一个做“入栈”,一个做“出栈”
  2. “出栈”为空,“入栈”数据导入到“出栈”,继续
public class WQueue {
    private Stack stack1 = new Stack();
    private Stack stack2 = new Stack();

    // 在队列尾部插入元素
    public void appendTail(int num) {
        stack1.push(num);
    }

    // 从队列顶部删除一个元素
    public Integer deleteHead() throws Exception {
        if (stack2.empty()) {
            while (!stack1.empty()) {
                stack2.push(stack1.pop()); // push入栈,pop出栈
            }
        }
        if (stack2.empty()) {
            throw new Exception("something wrong");
        }
        return stack2.pop();
    }
}
题7.1: 两个队列实现栈

思路:

  1. 任何时候都维持“一个队列空,另一个队列非空”
  2. 出栈时,把“非空队列”除队尾外的数据,导入“空队列”,删除“非空队列”对尾元素
// 两个队列实现栈
public class WStack {
    private Queue queue1 = new LinkedList();
    private Queue queue2 = new LinkedList();

    // 入栈
    public void append(int num) {
        if (queue1.isEmpty()) {
            queue2.offer(num); // offer入队列,poll出队列
        }else {
            queue1.offer(num);
        }
    }

    // 出栈
    public Integer delete() {
        Integer res = null;
        if (!queue1.isEmpty()) {
            while (queue1.size() > 1) {
                queue2.offer(queue1.poll());
            }
            res = queue1.poll();
        }else if (!queue2.isEmpty()) {
            while (queue2.size() > 1) {
                queue1.offer(queue2.poll());
            }
            res = queue2.poll();
        }
        return res;
    }
}

题9: 斐波那契数列

f(0) = 0
f(1) = 1
f(n) = f(n-1) + f(n-2)
解法一:递归

    // 斐波那契数列
    private static int fibo(int n) {
        if (n < 0) {
            return 0;
        }

        if (n <= 1) {
            return n;
        }

        return fibo(n - 1) + fibo(n - 2);
    }

上述解法存在很多重复计算,比如计算f(7),需要计算3次f(4),随着n的增大,重复计算节点会指数递增,而直接计算反而好些
解法二:直接计算

    // 斐波那契数列
    private static int fibo1(int n) {
        if (n < 0) {
            return 0;
        }

        if (n <= 1) {
            return n;
        }

        int n1 = 0;
        int n2 = 1;
        int sum = n1 + n2;
        for (int i = 2; i <= n; i++) {
            sum = n1 + n2;
            n1 = n2;
            n2 = sum;
        }
        return sum;
    }

很多问题可以转化为斐波那契数列,遇到问题先分析规律,先走一步找规律

题10: 二进制中1的个数

输入int数,输出二进制中1的个数
思路:

  1. 不能用除以2,因为负数的时候会出错,比如-1的二进制是32个1,应该输出32
  2. 每一位都做&运算
    // 二进制中1的个数
    private static int numOf1(int n) {
        int cnt = 0;
        int flag = 1;
        while (flag != 0) {
            if ((n & flag) == flag) {
                cnt++;
            }
            flag = flag << 1;
        }
        return cnt;
    }

tips:把一个整数减去1,在和原来的整数做&运算,得到的结果相当于把整数的二进制表示中最右边的一个1变为0,很多问题可以用这个思路解决

题40: 数组中只出现一次的数

描述:给定数组,只有两个数只出现一次,其余数均出现了两次,找出这两个数
思路:

  1. 任何一个数字异或它自己都等于0,异或满足结合律
  2. 如果数组中“只有一个数字只出现一次”,那么把整个数组异或一边,就能得到要找的数字
  3. 如果数组中“只有一个数字只出现一次”,尝试把数字分为两部分,每部分“只有一个数字出现一次”
  4. 这两个数字是不同的,因此他们异或的结果,二进制一定有一位是1,把这一位作为标识位,把数组分为两部分,问题解决
    // 数组中只出现一次的数字
    private static void findTwoInt(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }

        int sum = 0;
        for (int i = 0; i < arr.length; i++) {
            sum ^= arr[i];
        }

        // 找出key1 ^ key2 中为1的位(倒数第index位)
        int index = 1;
        while ((sum & 1) != 1) {
            sum = sum >> 1;
            index++;
        }

        int key1 = 0;
        int key2 = 0;
        for (int i = 0; i < arr.length; i++) {
            if (splitArr(arr[i], index)) {
                key1 ^= arr[i];
            }else {
                key2 = key2 ^ arr[i];
            }
        }
        System.out.println(key1 + " " + key2);
    }

    private static boolean splitArr(int num, int index) {
        num = num >> (index - 1);
        return (num & 1) == 1;
    }
题45: 约瑟夫问题

描述:0,1...n-1这n个人按顺序排成一个圆圈,从0开始报数,报到m-1的人退出,剩下的人继续从0开始报数,求最后胜利者的编号
比如:0,1,2,3,4这五个数,m=3,则依次删除2,0,4,1,最后3胜出
思路:总结出数学规律

  1. 用f(n)表示最后胜出的人
  2. 那么第一个一定是(m-1)%n退出,然后从k=m%n开始,继续报数
    k, k+1, k+2 ... n-2, n-1, 0, 1, 2 ... k-2
  3. 剩下的问题就完全变为了n-1个人的新约瑟夫环问题,我们把他们的编号做一下转换


    IMG_4119.JPG
  4. 原问题的解f(n),等于左边环的解(也就是第2步的环的解,这个好理解)
  5. 另外不管编号如何变化,最后胜出者的位置是不变的(也就是第3步,左右两个环的解位置相同)
  6. 左边编号x',到右边编号x,有映射关系x'=(x+k)%n
  7. 假设最后的解位于图标出的位置,即
    -- f(n)=x'
    -- f(n-1)=x
    -- x'=(x+k)%n
  8. 于是,f(n)=[f(n-1)+k]%n=[f(n-1)+m]%n (n>1的时候)
    // 约瑟夫环
    private static int lastWin(int n, int m) {
        if (n <= 0 || m <= 0) {
            return -1;
        }
        int last = 0;
        for (int i = 2; i <= n; i++) {
            last = (last + m) % i;
        }
        return last;
    }
题47: 不用加减乘除实现加法

思路:

  1. 二进制,加法用异或代替,进位用与代替
  2. 如果进位到了符号位,比如0x40000000+0x40000000,那么将得到0x80000000,也就是1073741824+1073741824=-2147483648,这是不合理的,但是如果直接用十进制加法得到的也是这个结果,因为这种情况溢出了,所以我们的算法还是正确的
    // 不用加减乘除做加法
    private static int sum(int i1, int i2) {
        int sum; // 不考虑进位的和
        int carry; // 进位
        do {
            sum = i1 ^ i2;
            carry = (i1 & i2) << 1;

            i1 = sum;
            i2 = carry;
        }while (carry != 0); // 直到没有进位
        
        return sum;
    }
题47.1: 不使用新的变量,交换两个变量的值

描述:交换a、b
思路:

  1. a=a+b; b=a-b; a=a-b;
  2. a=a^b; b=a^b; a=a^b; // 异或的结合率

题35: 第一个只出现一次的字符

思路:空间换时间

    // 第一个只出现一次的字符
    private static Character firstSingle(String str) {
        if (str == null || str.length() == 0) {
            return null;
        }

        int[] arr = new int[256]; // 下标-字符的asscii码,值-字符出现次数
        for (int i = 0; i < str.length(); i++) {
            arr[str.charAt(i)] ++;
        }

        for (int i = 0; i < str.length(); i++) {
            if (arr[str.charAt(i)] == 1) {
                return str.charAt(i);
            }
        }
        return null;
    }

题58: 二叉树的下一个节点

描述:给定一个二叉树的其中一个节点,树中的节点除了有指向左右子节点的指针,还有一个指向父节点的指针,找出中序遍历的下一个节点

题58: 判断一个二叉树是否对称

思路:定义一种遍历方式,根-右-左。判断先序遍历和此遍历得到的序列是否一致

    private static boolean isSymm(TreeNode root) {
        return isSymm(root, root);
    }

    private static boolean isSymm(TreeNode root1, TreeNode root2) {
        if (root1 == null && root2 == null) {
            return true;
        }
        if (root1 == null || root2 == null) {
            return false;
        }
        if (root1.data != root2.data) {
            return false;
        }
        return isSymm(root1.left, root2.right) && isSymm(root1.right, root2.left);
    }
题60: 二叉树打印成多行

描述:给定一个二叉树,每一层打印一行,每一层打印顺序从左到右
思路:用一个队列存放节点,用变量控制是否换行

    // 二叉树打印成多行
    private static void printTree(TreeNode root) {
        if (root == null) {
            return;
        }

        Queue queue = new LinkedList();
        queue.offer(root);
        int curUnHandle = 1; // 当前层未打印个数
        int nextTotal = 0; // 下一层总个数
        TreeNode node;
        while (!queue.isEmpty()) {
            node = queue.poll();
            System.out.print(node.data + " ");
            curUnHandle--;

            if (node.left != null) {
                queue.offer(node.left);
                nextTotal++;
            }
            if (node.right != null) {
                queue.offer(node.right);
                nextTotal++;
            }

            if (curUnHandle == 0) {
                System.out.println();
                curUnHandle = nextTotal;
                nextTotal = 0;
            }
        }
    }
题61: 之字形打印二叉树

描述:给定一个二叉树,按之字形打印二叉树,即第一行从左到右,第二行从右到左,依次类推
思路:分析规律,两个栈

    // 二叉树之字形打印
    private static void printTree2(TreeNode root) {
        if (root == null) {
            return;
        }

        Stack stack1 = new Stack(); // 先左后右
        Stack stack2 = new Stack(); // 先右后左
        stack1.push(root);
        TreeNode node;
        while (!stack1.empty() || !stack2.empty()) {
            if (!stack1.empty()) {
                while(!stack1.empty()) {
                    node = stack1.pop();
                    System.out.print(node.data + " ");
                    if (node.left != null) {
                        stack2.push(node.left);
                    }
                    if (node.right != null) {
                        stack2.push(node.right);
                    }
                }
                System.out.println();
            }else {
                while (!stack2.empty()) {
                    node = stack2.pop();
                    System.out.print(node.data + " ");
                    if (node.right != null) {
                        stack1.push(node.right);
                    }
                    if (node.left != null) {
                        stack1.push(node.left);
                    }
                }
                System.out.println();
            }
        }
    }
题62: 二叉搜索树的第k个节点

描述:给定二叉排序树,找出其中第k大的节点。
思路:二叉排序树的中序遍历是从小到大的数列

题62.1: 二叉树中序遍历
    // 二叉树中序遍历-递归
    private static void midPrintTree1(TreeNode node) {
        if (node == null) {
            return;
        }
        midPrintTree1(node.left);
        System.out.print(node.data + " ");
        midPrintTree1(node.right);
    }

    // 二叉树中序遍历-非递归
    private static void midPrintTree2(TreeNode node) {
        if (node == null) {
            return;
        }
        Stack stack = new Stack();
        while(node != null || !stack.empty()) {
            while(node != null) {
                // 前序遍历在这里打印即可
                stack.push(node);
                node = node.left;
            }
            if(!stack.empty()) {
                node = stack.pop();
                System.out.print(node.data + " ");
                node = node.right;
            }
        }
    }





你可能感兴趣的:(123)