面试/考试速查
常考数据结构类型速查速补表
* 单链表
双向链表
约瑟夫环
栈·栈实现计算器
* 前缀,中缀,后缀表达式,逆波兰计算器的实现
*递归,迷宫回溯,八皇后
排序算法基础
*冒泡排序
选择排序
插入排序
希尔排序
* 快速排序
归并排序
基数排序
各种排序的比较
二叉排序树
BST删除一棵子树的节点
* 二叉平衡树
* 图,图的深度优先和广度优先
* 动态规划
* 暴力匹配和KMP算法
* 贪心算法
带星的重要性不言而喻,需要重点掌握!
无法高效获取长度,无法根据偏移快速访问元素,是链表的两个劣势。然而面试的时候经常碰见诸如获取倒数第K个元素,获取中间位置的元素,判断链表是否存在环,判断环的长度等和长度与位置有关的问题。这些问题都可以通过灵活使用双指针来解决。
Tips:双指针并不是固定的公式,而是一种思维方式~
我们先来简单介绍一下何为快慢指针。快慢指针就是定义两根指针,移动的速度一快一慢,以此来制造出自己想要的差值。这个差值可以让我们找到链表上相应的节点。
思路:设有两个指针 p 和 q,初始时均指向头结点。首先,先让 p 沿着 next 移动 k 次。此时,p 指向第 k+1个结点,q 指向头节点,两个指针的距离为 k 。然后,同时移动 p 和 q,直到 p 指向空,此时 p 即指向倒数第 k 个结点。
//获取倒数第K个元素
//思路:两个指针同时指向头部,将一个指针fast先移动K次 再将快慢指针同时移动 直到fast指针到达尾部时 返回slow的那个节点的值即可
public static ListNode getLastNode(ListNode head, int k) {
//判断k
if (k < 0) {
return null;
}
//定义快慢指针
ListNode fast = head;
ListNode slow = head;
//将fast移动K次
while (k != 0) {
fast = fast.next;
k--;
}
//fast 不为空时 fast slow 一起移动
while (fast != null){
fast = fast.next;
slow = slow.next;
}
return slow;
}
一般的思路 : 先遍历一次链表 记住链表一共有多少个节点,然后在遍历 寻找中间值
利用快慢指针,思路如下: 把一个链表看作是一个跑道,假设a的速度是b速度的两倍,当a跑完全程后,b正好跑完一半,以此来得到中间值。
快慢指针:slow和fast都指向第一个节点,fast一次移动2个节点,slow一次移动1个节点。
n为偶数:中间数靠后
//寻找中间值
//思路:假设两个指针都指向头节点 fast指针移动速度是slow指针移动速度的两倍 例如 fast一次移动2个节点 slow 一次移动一个节点
//当fast移动到尾部(为空)时,slow 正好处于一半的位置 为中间值
public static ListNode findMid(ListNode head){
ListNode fast = head;
ListNode slow = head;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
【注】快指针一次一次移动 移动之前判断是否为空
public static class ListNode{
int val;
ListNode next;
ListNode(int x){
val = x;
next = null;
}
}
private static boolean hasCycle(ListNode head){
//定义快慢指针
ListNode fast = head;
ListNode slow = head;
//判断
while(fast != null){
fast = fast.next;
if(fast != null){
fast = fast.next;
}
if(fast == slow){
return true;//有环
}
slow = slow.next;
}
return false;//没有环
}
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 = new ListNode(5);
System.out.println(hasCycle(head));//false
head.next.next.next.next.next = head.next;
System.out.println(hasCycle(head));//true
}
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
思路:定义两个指针 cur指向头节点 pre指向null(头节点的前一个节点) 一个用于交换数据的节点tmp ,先将cur的下一个节点保存在tmp节点中,再将cur指向pre(pre = cur.next ),cur和pre同时向后移动(交换数据)
//反转链表
//思路:双指针迭代 遍历链表 将当前节点指向前一个节点 需要临时保存当前节点的下一个节点
public static ListNode reverseList(ListNode head) {
ListNode cur = head;
ListNode pre = null;
ListNode tmp = null;
while (cur != null) {
tmp = cur.next;//保存cur的下一个节点
cur.next = pre;//当前节点指向pre
pre = cur;//向后移
cur = tmp;//向后移
}
return pre;
}
左链表:1->2->3 右链表:4->5 合并后:1->5->2->4->3
思路:将右链表的第一个节点插入到左链表的第二个节点。左链表变成: 1->5->2->3 右链表 4 此时leftTemp要指向 2 这个节点 right.next = leftTemp ,将左右链表向后移动即可
public static ListNode merge(ListNode left,ListNdoe right){
while(left.next != null && right != null){
ListNode leftTemp = left.next;
ListNode rightTemp = right.next;
left.next = right;
right.next = leftTemp;
left = leftTemp;
right = rightTemp;
}
}
输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
思路: 定义两个指针指向新的一个链表的头节点(0)cur pre(指向头部),一个用来保存是否进位的变量carry
将sum = l1+l2+carry,
sum = sum % 10 个位数保存再sum中 十位保存在carry中 ,
sum 加入cur.next = new ListNode(sum)
cur向后移动 cur = cur.next;
l1 l2 都向后移动
如果有一个链表为空 且carry = 1 则有进位 要再尾部新创建一个节点 cur.next = new ListNode(carry)
返回新的链表 pre.next
public static ListNode addTwoNumber(ListNode l1,ListNode l2){
int carry = 0;
ListNode pre = new ListNode(0);
ListNode cur= pre;
while(l1 != null || l2 != null){
int x = l1 == null ? 0 : l1.val;
int y = l2 == null ? 0 : l2.val;
int sum = x + y + carry;
carry = sum / 10;
sum = sum % 10;
cur.next = new ListNode(sum);
cur = cur.next;
if(l1 != null){
l1 = l1.next;
}
if(l2 != null){
l2 = l2.next;
}
}
if(carry == 1){
cur.next = new ListNode(carry);
}
return pre.next;
}
思路: 将待删除的节点的下一个节点的值赋给待删除的节点,将待删除的节点指向待删除节点的下下一个节点 将待删除下一个节点的值赋给待删除节点 实际上删除的是下一个节点
public static void deleteNode(ListNode node){
node.val = node.next.val;
node.next = node.next.next;
}
思路:先找到倒数第K个元素,删除即可(slow.next = slow.next.next)
Tips: 用到预先指针。对于链表问题,返回结果为头结点时,通常需要先初始化一个预先指针 pre,该指针的下一个节点指向真正的头结点head。使用预先指针的目的在于链表初始化时无可用节点值,而且链表构造过程需要指针移动,进而会导致头指针丢失,无法返回结果。
将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
思路:递归
public static ListNode mergeTwoLists(ListNode l1, ListNode l2){
if(l1 == null){
return l2;
}
if(l2 == null){
return l1;
}
if(l1.val < l2.val){
l1.next = mergeTwoLists(l1.next,l2);
return l1;
}else{
l2.next = mergeTwoLists(l1,l2.next);
return l2;
}
}
非递归:定义一个新的链表(预先指针pre 需要通过头节点返回链表 首先考虑预先指针 还要一个临时指针 用来遍历整个链表的位置) 判断l1、l2 两个链表的第一个的大小 小的加到新链表上,新链表指针向后移动,小的节点链表也想后移动 直到一方为空 再将另一链表加入新链表中(因为是有序链表无需比较) 迭代
public static ListNode megerTwoLists(ListNode l1, ListNode l2){
ListNode pre = new ListNode(0);
ListNode temp = pre;
while(l1 != null && l2 != null){
if(l1.val < l2.val){
temp.next = l1;
temp = temp.next;
l1 = l1.next;
}
else{
temp.next = l2;
temp = temp.next;
l2 = l2.next;
}
}
if(l1 == null){
temp.next = l2;
}else{
temp.next = l1;
}
return pre.next;
}