面试遇到的算法题
一、链表相关
1、单链表倒序
定义链表节点Node
public class Node { private int index; //当前节点值 public Node next; public Node(NodeBuilder builder) { this.index = builder.getIndex(); this.next = builder.getNext(); } public static NodeBuilder newBuilder() { return new NodeBuilder(); } }
Node建造器
public class NodeBuilder { private int index; private Node next; public int getIndex() { return index; } public NodeBuilder setIndex(int index) { this.index = index; return this; } public Node getNext() { return next; } public NodeBuilder setNext(Node next) { this.next = next; return this; } public Node builder(){ return new Node(this); } }
反转链表节点算法:
思想:
1、定义一个新的链表newNode,开始为null,作为反转后的链表输出
2、遍历旧的链表oldNode(拿到链表的首节点,并将剩余的链表nextNode作为新链表进行下一轮循环,直到剩余的链表为null为止),
3、将每次拿到的链表的首节点依次作为新链表newNode的上一个节点(实现方式为每次循环都将剩余链表的下一个节点指向为newNode)
public class ReverseLinkedlist { /** * 反转单链表 1->2->3->4->null 转成 4->3->2->1->null * * @param node * @return */ static Node reverse(Node node) { // 反转后的新的链表,开始为null Node newNode= null; // 保存剩下的链表 Node next = null; // 2->3->4->null、3->4->null、4->null、null while (node != null) { next = node.next; // 临时变量保存链表的下一个节点 node.next = newNode; // 第一次将1->null,接着2->1->null,3->2->1->null,4->3->2->1->null newNode= node; // 将调整好的node赋值给newNode,作为新的链表最后输出 node = next; // 将临时变量保存的剩下的链表赋值给node,进行下一个循环 } return newNode; } public static void main(String[] args) { Node node4 = Node.newBuilder().setNext(null).setIndex(4).builder(); Node node3 = Node.newBuilder().setNext(node4).setIndex(3).builder(); Node node2 = Node.newBuilder().setNext(node3).setIndex(2).builder(); Node node = Node.newBuilder().setNext(node2).setIndex(1).builder(); Node newNode = reverse(node); } }
递归方式:
/** * 递归方式反转单链表 * * @param node */ static Node reverse2(Node node) { if (node.next == null) { System.out.println(node); return node; } Node next=node.next;//保存当前节点的下一个节点 2->3->4->null,3->4->null,4->null,最后一次递归之后 // ,res局部变量指向4->null在堆中的地址值,后面通过修改next所以res也随之改变 // res=next=4->null System.out.println(next); Node head = node;// 保存当前节点 Node res = reverse2(node.next); next.next = head; head.next = null; return res;// 4->null,4->3->null,4->3->2->null,4->3->2->1->null }
递归方式2:
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null) {
return head;
}
ListNode temp = reverseList(head.next);
head.next.next = head;
head.next = null;
return temp;
}
}
2、判断单链表是否有环
解题思路:双指针法
数学上的追及问题,跑道时环形的,同时出发的两个运动员,跑的快的运动员必然会比跑的慢的在多跑一圈相遇。
那么定义两个指针p1和p2,开始同时指向头节点,然后p1每次向后移动一个节点,p2每次向后移动两个节点,然后比较两个指针指向的节点是否相同,若相同,则可以判断出链表有环。
static boolean isCycle(Node node) { Node p1 = node; Node p2 = node; while (p2 != null && p2.next != null) { p1 = p1.next; p2 = p2.next.next; if (p1 == p2) { return true; } } return false; }
求出环的长度:
思路,在第一次相遇后继续前进,第二次相遇时,p1的前进次数就是环的长度
static int cycleLength(Node node) { Node p1 = node; Node p2 = node; int length = 0;// 环长度 int meetTime = 0;//相遇次数 while (p2 != null && p2.next != null) { if (meetTime == 1) { length++; } p1 = p1.next; p2 = p2.next.next; if (p1 == p2) { meetTime++; } if (meetTime == 2) { break; } } return length; }
二、数组相关
1、将数组内的值为x的元素移动到末尾
思路:双指针法,一个指向开头left,一个指向结尾right
①:right从尾部向左移动,碰到第一个值不等于x的元素,停下来
②:left从头向右移动,碰到值等于x的元素,停下来
③:left和right指针指向的值交换
④:循环直到left和right相遇
/** * 将数组中的0移动到尾部 * 〈功能详细描述〉 * * @author 17090889 * @see [相关类/方法](可选) * @since [产品/模块版本] (可选) */ public class arrayMove { /** * 使用快排中的哨兵思想,左哨兵往右移动,右哨兵往左移动 * 左哨兵移动到值为0的元素停下来,右哨兵移动到值不为0的元素停下来,两边交换值 * 继续移动,直到左右哨兵相遇 * * @param a */ public static void sort(int[] a) { int len = a.length; int i = 0; int j = len - 1; while (i < j) { while (a[i] != 0 && i < j) { i++; } while (a[j] == 0 && i < j) { j--; } // 交换元素 if (i < j) { int temp = a[j]; a[j] = a[i]; a[i] = temp; } } } public static void main(String[] args) { int[] a = {2, 0, 7, 9, 0, 8, 10, 0, 14, 13, 11, 0}; sort(a);//2,11,7,9,13,8,10,14,0,0,0,0 } }
2、一个无序数组,求出该数组排序后的任意两个相邻元素的最大差值,要求时间复杂度和空间复杂度尽可能低。
解法:
step1:使用桶排序思想,将元素按照数组的长度分成n个桶,一个桶代表一个区间范围,其中第一个桶从数组的最小值min开始,区间跨度是(max-min)/(n-1)
这一步需要遍历整个数组,得到数组的最大值和最小值,时间复杂度为O(n)
step2:遍历原数组,把原数组每一个元素插入到对应的桶中,记录每一个桶的最大值和最小值,时间复杂度为O(n)
step3:遍历所有的桶,统计出每一个桶的最大值和这个桶的最小值的差,数值最大的差值就是原数组排序后的相邻最大差值
三、栈相关
1、实现一个栈,该栈有出栈(pop)、入栈(push)、取最小元素(getMin)3个方法,要保证这3个方法的时间复杂度都是O(1)
问题实现思路:
1)、同时创建两个栈AB,A用来存储实际元素,B用来存储栈中的最小元素
2)、当第一个元素入栈时,同时存入这两个栈中,此时这个栈B的这个元素标志为栈A中的最小值
3)、后面的每一个元素入栈A,都与栈B的栈顶元素相比较,若小于或等于,则将其压入栈B栈顶
4)、栈A的每一个出栈元素,若和栈B栈顶的元素相同,则将栈B栈顶的元素也出栈
5)、栈A中最小的元素即为栈B栈顶的元素
public class MinStack { Stack<Integer> stackA = new Stack(); Stack<Integer> stackB = new Stack(); /** * 入栈 * @param e */ public void push(int e) { stackA.push(e); // 若栈B为空或者入栈元素的值小于等于栈B栈顶元素的值,则入栈 if (stackB.isEmpty() || e <= stackB.peek()) { stackB.push(e); } } /** * 出栈 */ public Integer pop() { Integer e = stackA.pop(); if (stackB.peek().equals(e)) { stackB.pop(); } return e; } /** * 获取最小值 */ Integer getMin() { if (!stackB.isEmpty()) { return stackB.peek(); } return null; } }
补充:
JDK stack源码:
public class Stack<E> extends Vector<E> { // 使用数组实现栈 // 构造一个空栈 public Stack() { } // 压入栈顶部 public E push(E item) { addElement(item); return item; } // 移除栈顶的对象,并返回 public synchronized E pop() { E obj; int len = size(); obj = peek(); removeElementAt(len - 1); return obj; } // 查看栈顶的对象,但不从栈中移除它 public synchronized E peek() { int len = size(); if (len == 0) throw new EmptyStackException(); return elementAt(len - 1); } // 判断栈是否为空 public boolean empty() { return size() == 0; } //返回对象在栈中的位置,以 1 为基数。 public synchronized int search(Object o) { int i = lastIndexOf(o); if (i >= 0) { return size() - i; } return -1; } }
2、用栈实现队列:
两个栈,A入栈B出栈
当有出栈操作时,判断栈B是否有元素,若有,则将栈B栈顶元素出栈,若无,则将栈A的元素全部出栈然后入栈B,再从栈B出栈。
四、队列相关
五、树相关
1、二叉树遍历
public class TreeTraversal { /** * 前序遍历,根左右 */ public static void preTraversalTree(TreeNode node) { if (node == null) { return; } System.out.println(node.data); preTraversalTree(node.leftChild);// 左子节点不断入栈,直到没有左子节点,再挨个输出结果 preTraversalTree(node.rightChild); } /** * 中序遍历,左根右 * @param node */ public static void midTraversalTree(TreeNode node) { if (node == null) { return; } midTraversalTree(node.leftChild); System.out.println(node.data); midTraversalTree(node.rightChild); } /** * 后序遍历,左右根 * @param node */ public static void finTraversalTree(TreeNode node) { if (node == null) { return; } finTraversalTree(node.leftChild); finTraversalTree(node.rightChild); System.out.println(node.data); } /** * 树节点 */ static class TreeNode { int data; TreeNode leftChild; TreeNode rightChild; } }
其实递归的双次调用就是等前一个全部入栈后,每一次出栈,再调用后一个入栈。二叉树的深度优先搜索依赖栈。
层序遍历-广度优先搜索,依赖队列,根节点先入队,然后出队,每个节点出队后,将其左右子节点依次入队。直到队列为空为止。
/** * 层序遍历 */ public static void sequenceTraversal(TreeNode node) { // 创建队列用于临时保存节点 Queue<TreeNode> queue = new LinkedList(); // 根节点入队 queue.offer(node); // 依次出队 while (!queue.isEmpty()) { TreeNode head=queue.poll(); System.out.println(head.data); // 左右子节点依次入队 if(head.leftChild!=null){ queue.offer(head.leftChild); } if(head.rightChild!=null){ queue.offer(head.rightChild); } } }
2、求一颗二叉树的最近的公共父节点
1):若这棵树是一颗二叉查找树(左子树上的值都小于父节点的值,右子树上的值都大于父节点的值)
/** * 求两个节点的最近父节点,若这棵树是一颗二叉查找树 * 那么根节点的值和两个节点的值进行比较,若一个大于,一个小于,那么根节点就是公共节点 * 若都小于,则遍历左子树,若都大于则遍历右子树 * 直到找到一个节点的值在要查找的两个值的中间即可 */ public static void recentParent(TreeNode node, TreeNode p, TreeNode q) { if (node == null) { return; } // 先获取根节点的值 int data = node.data; int pdata = p.data; int qdata = q.data; boolean flag = (data <= qdata && pdata <= data || data >= qdata && pdata >= data); if (flag) { System.out.println(node.data); return; } else if (data > pdata && data > qdata) { recentParent(node.leftChild, p, q); } else { recentParent(node.rightChild, p, q); } }
2):若这是一棵普通的二叉树
public static TreeNode recentParent2(TreeNode node, TreeNode p, TreeNode q) { if (node == null) { return null; } if (node.data == p.data || node.data == q.data) { return node; } // 遍历所有左子树 TreeNode left = recentParent2(node.leftChild, p, q); // 遍历所有右子树 TreeNode right = recentParent2(node.rightChild, p, q); if (left != null && right != null) { return node; } if (left != null) { return left; } else if (right != null) { return right; } else { return null; } }
六、数学相关
1、求最大公约数:
1)欧几里得算法:改算法的目的是求出两个数的最大公约数。基于一个定理:两个正整数a和b(a>b),它们的最大公约数等于a除以b的余数c和b之间的最大公约数。于是可是通过递归的方式逐步简化,直到两个数可以整除或者其中一个数为1
/** * 欧几里得算法,两个数的最大公约数等于两个数的余数和小的一个数的最大公约数 * 但是当两个数比较大时,取余%性能较差 */ public static int gcd(int a, int b) { int max = a > b ? a : b; int min = a > b ? b : a; if (max % min == 0) { return min; } return gcd(min, max % min); }
2)更相减损术:两个正整数a和b的最大公约数等于a-b的差值和较小数b的最大公约数,直到最后两个数相等为止,这个数就是最大公约数
缺点:当两个数相差很大,比如求1000和1之间的最大公约数就要递归9999次。
public static int gcd2(int a, int b) { // 两数相等求得最大公约数 if (a == b) { return a; } int max = a > b ? a : b; int min = a > b ? b : a; return gcd(min, max % min); }
3)最终解法,使用更相减损术和移位相结合。
①:当a,b都为偶数时,gcd(a,b)=2*gcd(a>>1,b>>1)=2*gcd(a>>1,b>>1)
②:当a为偶数,b为奇数时,gcd(a,b)=gcd(a>>1,b)
③:当a为奇数,b为偶数时,gcd(a,b)=gcd(a,b>>1)
④:当a,b都为奇数时,先利用更相减损术计算一次,gcd(a,b)=gcd(b,a-b),此时a-b必为偶数,再继续进行移位运算即可
判断是否为奇偶数可以通过与&运算符,让整数和1进行与&运算,如果a&1==0,则a是偶数,如果a&1!=0,则a是奇数。
/** * 求两数的最大公约数 * 欧几里得算法:两个数的最大公因数是较大数和较小数的余数与较小数的最大公因数,直到两数能整除为止 * 更相减损术:两个数的最大公因数是较大数和较小数的差和最小数的最大公因数,两数相等为止 * <p> * 〈功能详细描述〉 * * @author 17090889 * @see [相关类/方法](可选) * @since [产品/模块版本] (可选) */ public class Gcd { /** * 更相减损术和移位运算结合减少相减次数 */ public static int gcd(int a, int b) { if (a == b) { return a; } if ((a & 1) == 0 && (b & 1) == 0) { return gcd(a >> 1, b >> 1) << 1; } else if ((a & 1) == 0 && (b & 1) != 0) { return gcd(a >> 1, b); } else if ((a & 1) != 0 && (b & 1) == 0) { return gcd(a, b >> 1); } else { int max = a > b ? a : b; int min = a > b ? b : a; return gcd(max - min, min); } } }
2、判断一个数是不是2的次幂
思路:凡是2的整数次幂和它本身减1的结果进行与运算的结果都必然是0,反之,必然不是0。
public static boolean is2Power(int a) { return (a & a - 1) == 0; }
3、判断一个数是不是偶数
思路:偶数和1做与&运算为0
4、给出一个正整数,找出这个这个正整数所有数字全排列的下一个数,即全排列的大于这个数的最小的数。
一组正整数排列的最大数即这组数按照逆序排列,最小数即这组数的正序排列
那么如何找到一组全排序数的和它最接近的全排列数呢?原则是尽量保持高位(左)不变,低位在最小的范围内变换顺序,变换顺序的范围大小取决于当前整数的逆序区域。
步骤:
1、从后向前查看逆序区域,即找到从低位渐增的范围,然后找到逆序区域的前一位
2、让逆序区域的前一位和逆序区域中大于它的最小数交换位置
3、将原来的逆序区域按照递增排序
这种解法叫做字典序算法。
5、删去k个数后的剩余数的最小值,删除一个数后,其后面的数递补其位置。
思路:删除一个数后必然少一位,那么优先需要考虑的是将高位的数字降低。如何降低高位数字呢?将一组数从高位依次和其相邻的后一位进行比较,如果有一位的数大于
其后一位的数,那么删除该数字后,后位递补必然使该数位的值降低。
那么对于删除k个数,只要依次从要删除的数高位和后一位进行比较,找到大于后一位的高位数,将其删除。然后将剩下的数再按照此方式进行删除,直到删掉k个位为止,最
后得到的数一定是最小的。若从高位开始比较,直到最后一位都没有找到比其小的后一位数,则将最后一位删除即可,如1234567,删除7。
可以借助栈,从高位依次入栈,入栈前和栈顶的数字比较,若栈顶的数字大于要入栈的数字,则将栈顶的数字删除,出栈k次,最后栈中的数逆序就是最小数。
这种每一次求得局部最优解,最终得到全局最优解的思想,叫做贪心算法。
6、寻找缺失的整数
1)在一个无序数组中有99个不重复的正整数,范围从1到100,找到缺失的那一个整数。
解法:1-100的和减去数组中全部元素的和,得到的结果就是缺失的那个整数
2)在一个有序的数组中有99个不重复的正整数,范围从1到100,找到缺失的那一个整数
解法:利用数组下标和元素的对应关系,使用二分查找法,找到第一个下标和对应元素值相同的那个值
3)一个无序数组中又若干个正整数,范围是1~100,其中99个整数都出现了偶数次,只有一个整数出现了基数次,如何找到这个出现了奇数次的整数?tips:1^1=0
解法:利用一个数和其本身做异或操作的结果为0。那么只要遍历数组依次做异或运算,得到的结果就是出现奇数次的整数。时间复杂度为O(n),空间复杂度为O(1)
END.