目录
一.哈希表的简单介绍
- 哈希表在使用层面可以理解为一种集合结构
- 如果只有key,没有伴随数据value,可以使用HashSet结构(C++中叫UnOrderedSet)
- 如果既有key,又有伴随数据value,可以使用HashMap结构(C++中叫UnOrderedMap)
- 有无伴随数据,是HashMap和HashSet唯一区别,底层的实际结构式一回事
- 使用哈希表增(put/add)、删(remove)、改(put)和查(get)操作,可以认为时间复杂度为O(1),但是常熟时间比较大
- 放入哈希表的东西,如果是基础类型,内部按值传递,内存占用就是这个东西的大小
- 放入哈希表的东西,如果不是基础类型,内部按引用传递,内存占用式这个东西内存地址的大小
二.有序表的简单介绍
- 有序表在使用层面上可以理解为一种集合结构
- 如果只有key,没有伴随数据value,可以使用TreeSet结构(C++中叫OrderedSet)
- 如果既有key,又有伴随数据value,可以使用treeMap结构(C++中叫OrderedMap)
- 有无伴随数据,是TreeSet和TreeMap唯一的区别,底层的实际结构是一回事
- 有序表和哈希表的区别是,有序表把key按照顺序组织起来,而哈希表完全不组织
- 红黑树、AVL树、size-balance-tree和跳表等都属于有序表结构,只是底层具体实现不同
- 放入有序表的东西,如果是基础类型,内部按照值传递,内存占用就是这个东西的大小
- 放入有序表的东西,如果不是基础类型,必须提供比较器,内部按照引用传递,内存占用是这个东西内存的大小
- 不管是什么底层具体实现,只要是有序表,都是一下固定的基本功能和固定的时间复杂度
- void put(K key, V value):将一个(key,value)记录加入到表中,或者将key的记录更新成value
- V get(K key):根据给定的key,查询value并返回
- void remove(K key):溢出key的记录
- boolean containsKey(K key):查询时否关于key的记录
- K firstKey():返回所有键值的排序结果中,最左(最小)的那个
- K lastKey():返回所有键值的排序结果中,最右(最大)的那个
- K floorKey(K key):如果表中存入过key,返回key;否则返回所有键值排序结果中,key的前一个
- K ceilingKey(K key):如果表中存入过key,返回key;否则返回所有键值排序结果中,key的后一个
以上所有操作时间复杂度时O(logN),N为有序表含有的记录数
三.链表
1.单链表结构
public static class Node{
int value;
Node next;
}
反转单向链表:要求时间复杂度O(N),额外空间复杂度O(1)
public static class Node {
public int value;
public Node next;
public Node(int data) {
this.value = data;
}
}
public static Node reverseList(Node head) {
Node pre = null;
Node next = null;
while (head != null) {
next = head.next;
head.next = pre;
pre = head;
head = next;
}
return pre;
}
2.双链表
public static class Node{
int value;
Node next;
Node last;
}
反转双向链表:要求时间复杂度O(N),空间复杂度O(1)
public static class DoubleNode {
public int value;
public DoubleNode last;
public DoubleNode next;
public DoubleNode(int data) {
this.value = data;
}
}
public static DoubleNode reverseList(DoubleNode head) {
DoubleNode pre = null;
DoubleNode next = null;
while (head != null) {
next = head.next;
head.next = pre;
head.last = next;
pre = head;
head = next;
}
return pre;
}
3.判断一个链表是否是回文结构
题目:给定一个但蛋链表的头节点head,请判断该链表是否为回文结构
例子:1->2->1 返回true 1->2->2->1 返回true 15->6->15 返回true 1->2->3 返回false
要求:
对于笔试,如果链表长度为N,时间复杂度达到O(N),额外空间复杂度达到O(N)
对于面试,如果链表长度为N,时间复杂度达到O(N),额外空间复杂度达到O(1)
将链表依次压入栈中,然后不断弹栈并重前先后依次比较依次,如果全部一样就是回文。
更省空间的做法,只将链表有伴部分压栈,然后弹栈并依次和左半部分从左到右比较,如果组后栈空,说明是回文,相比上面的做法省了一半的栈空间。
首先使用快慢指针,当快指针指向结尾时,慢指针指向中间位置。同时要设法将右半部分的指针全部反向,同时记录头元素和最后一个元素的位置,然后同时向中间靠拢,逐一比较是否相等来判断是否时回文,判断结束后还要设法将原先方向的指针返回去。(这样实现的空间复杂度是O(1))
// need O(1) extra space
public static boolean isPalindrome3(Node head) {
if (head == null || head.next == null) {
return true;
}
Node n1 = head;
Node n2 = head;
while (n2.next != null && n2.next.next != null) { // find mid node
n1 = n1.next; // n1 -> mid
n2 = n2.next.next; // n2 -> end
}
n2 = n1.next; // n2 -> right part first node
n1.next = null; // mid.next -> null
Node n3 = null;
while (n2 != null) { // right part convert
n3 = n2.next; // n3 -> save next node
n2.next = n1; // next of right node convert
n1 = n2; // n1 move
n2 = n3; // n2 move
}
n3 = n1; // n3 -> save last node
n2 = head;// n2 -> left first node
boolean res = true;
while (n1 != null && n2 != null) { // check palindrome
if (n1.value != n2.value) {
res = false;
break;
}
n1 = n1.next; // left to mid
n2 = n2.next; // right to mid
}
n1 = n3.next;
n3.next = null;
while (n1 != null) { // recover list
n2 = n1.next;
n1.next = n3;
n3 = n1;
n1 = n2;
}
return res;
}
4.将单向链表按照某值划分成左边小,中间相等,右边大的形式
题目:
给定一个单链表的头节点head,节点的值类型是整型,再给定一个整数pivot。实现一个调整链表的函数,将链表调整为左部分都是值小于pivot的节点,中间部分都是只等于pivot的节点,右部分是值大于pivot的节点。
【进阶】在实现原问题功能的基础上增加如下的要求
【要求】调整后所有小于pivot的节点之间的相对顺序和调整之前一样
【要求】调整后所有等于pivot的节点之间的相对顺序和调整之前一样
【要求】调整后所有大于pivot的节点之间的相对顺序和调整之前一样
【要求】时间复杂度请达到O(N),额外空间复杂度请达到O(1)
如果是笔试,将节点全部放到数组中,在数组中用patatition,最后将数组中的数用节点串起来。
如果面试,不使用额外空间,仅需要6个变量。
链表比较灵活,可以使用用6个指针(<区域的头尾指针,=区域的头尾指针,>区域的头尾指针)维护几个指针,依次将数接到对应区域尾部,最后将三块区域串起来即可。
连接三个区域的时候一定要充分讨论是否有<\=\>区域。
public static Node listPartition2(Node head, int pivot) {
Node sH = null; // small head
Node sT = null; // small tail
Node eH = null; // equal head
Node eT = null; // equal tail
Node bH = null; // big head
Node bT = null; // big tail
Node next = null; // save next node
// every node distributed to three lists
while (head != null) {
next = head.next;
head.next = null;
if (head.value < pivot) {
if (sH == null) {
sH = head;
sT = head;
} else {
sT.next = head;
sT = head;
}
} else if (head.value == pivot) {
if (eH == null) {
eH = head;
eT = head;
} else {
eT.next = head;
eT = head;
}
} else {
if (bH == null) {
bH = head;
bT = head;
} else {
bT.next = head;
bT = head;
}
}
head = next;
}
// small and equal reconnect
if (sT != null) {
sT.next = eH;
eT = eT == null ? sT : eT;
}
// all reconnect
if (eT != null) {
eT.next = bH;
}
return sH != null ? sH : eH != null ? eH : bH;
}
5.复制还有随机指针节点的链表
题目:
一种特殊的单链表节点类描述如下:
class Node{
int value;
Node next;
Node rand;
Node(int val){
value=val;
}
}
rand指针是单链表节点结构中新增的指针,rand可能指向链表中的任意一个节点,也可能指向null。给定一个由Node节点类型组成的无环单链表的头节点head,请实现一个函数完成这个链表的复制,并返回复制的新链表的头节点。
【要求】时间复杂度O(N),额外空间复杂度O(1)
这里利用了哈希表实现了老节点和克隆节点的一一对应,随后遍历一遍老链表,根据老节点的连接关系为新链表构建相同的连接关系。
public static class Node {
public int value;
public Node next;
public Node rand;
public Node(int data) {
this.value = data;
}
}
public static Node copyListWithRand1(Node head) {
HashMap<Node, Node> map = new HashMap<Node, Node>();
Node cur = head;
while (cur != null) {
map.put(cur, new Node(cur.value));
cur = cur.next;
}
cur = head;
while (cur != null) {
map.get(cur).next = map.get(cur.next);
map.get(cur).rand = map.get(cur.rand);
cur = cur.next;
}
return map.get(head);
}
如上图所示,也可以不用使用HashMap实现链表的复制
首先,将每个节点的克隆节点插入在cur和cur.next之间(此时cur.next就是对应节点的克隆节点)
随后,将节点一对一对处理rand指针,比如1节点的rand指向3,1的克隆节点是1.next,3的克隆节点是3,next,则可以将1的克隆节点的rand指针指向3的克隆节点。其他节点如上处理。
最后,将克隆链表从其中分离出来即可。
public static Node copyListWithRand2(Node head) {
if (head == null) {
return null;
}
Node cur = head;
Node next = null;
// copy node and link to every node
while (cur != null) {
next = cur.next;
cur.next = new Node(cur.value);
cur.next.next = next;
cur = next;
}
cur = head;
Node curCopy = null;
// set copy node rand
while (cur != null) {
next = cur.next.next;
curCopy = cur.next;
curCopy.rand = cur.rand != null ? cur.rand.next : null;
cur = next;
}
Node res = head.next;
cur = head;
// split
while (cur != null) {
next = cur.next.next;
curCopy = cur.next;
cur.next = next;
curCopy.next = next != null ? next.next : null;
cur = next;
}
return res;
}
6.两个单链表相交的一系列问题
题目:
给定两个可能有环也可能无环的单链表,头节点head1和head2。请实现一个函数,如果两个链表相交,请返回相交的第一个节点。如果不相交,返回null。
【要求】如果两个链表长度之和为N,时间复杂度请达到O(N),额外空间复杂度达到O(1)
判断一个链表是否有环:有环则返回进入环的第一个节点
可以使用哈希表来找到第一个入环节点, 如果一个链表有环,一定有会重复记录多个节点,当第一次发现一个节点已经在哈希表里,那么这个节点就是入环节点。
利用快慢指针判断是否有环,同时返回第一个入环的节点。(额外的空间复杂度为O(1))
如上图S所在位置是快慢指针相遇的位置。
魔性的结论:慢指针S从相遇位置开始,快指针F从头开始,F和S指针都每次走一步,当F和S相遇的节点就是第一个入环节点。
public static Node getLoopNode(Node head) {
if (head == null || head.next == null || head.next.next == null) {
return null;
}
Node n1 = head.next; // n1 -> slow
Node n2 = head.next.next; // n2 -> fast
while (n1 != n2) {
if (n2.next == null || n2.next.next == null) {
return null;
}
n2 = n2.next.next;
n1 = n1.next;
}
n2 = head; // n2 -> walk again from head
while (n1 != n2) {
n1 = n1.next;
n2 = n2.next;
}
return n1;
}
情况一:链表一和链表二都无环
分别记录两个链表的终止节点,如果两个链表的终止节点是同一个节点,说明两个链表有相交。那么如何确定第一个相遇的节点呢?分别算出链表一长度len1和链表二长度len2,让短一点的链表先走|len1-len2|差值步,随后一起走,相遇的节点就是所求的相交的节点。
public static Node noLoop(Node head1, Node head2) {
if (head1 == null || head2 == null) {
return null;
}
Node cur1 = head1;
Node cur2 = head2;
int n = 0;
while (cur1.next != null) {
n++;
cur1 = cur1.next;
}
while (cur2.next != null) {
n--;
cur2 = cur2.next;
}
if (cur1 != cur2) {
return null;
}
cur1 = n > 0 ? head1 : head2;
cur2 = cur1 == head1 ? head2 : head1;
n = Math.abs(n);
while (n != 0) {
n--;
cur1 = cur1.next;
}
while (cur1 != cur2) {
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
}
情况二:一个链表无环,一个链表有环
不存在这样的情况。
情况三:两个链表都有环
如何区分三种情况:(loop1、loop2分别是两个有环链表的第一个入环节点)
若loop1==loop2则是情况二,那么如何找到第一个相交的节点呢?可以把第一个入环节点看成两个链表的终止位置 => 变成了求两个无环链表第一个相交的节点
若loop1!=loop2则是情况一或情况三,那么如何区分这两种情况呢? 可以发现如果从loop1出发回到loop1的过程中遇到loop2则是情况三,遇不到则是情况一。
public static Node bothLoop(Node head1, Node loop1, Node head2, Node loop2) {
Node cur1 = null;
Node cur2 = null;
if (loop1 == loop2) {
cur1 = head1;
cur2 = head2;
int n = 0;
while (cur1 != loop1) {
n++;
cur1 = cur1.next;
}
while (cur2 != loop2) {
n--;
cur2 = cur2.next;
}
cur1 = n > 0 ? head1 : head2;
cur2 = cur1 == head1 ? head2 : head1;
n = Math.abs(n);
while (n != 0) {
n--;
cur1 = cur1.next;
}
while (cur1 != cur2) {
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
} else {
cur1 = loop1.next;
while (cur1 != loop1) {
if (cur1 == loop2) {
return loop1;
}
cur1 = cur1.next;
}
return null;
}
}