0.特殊
0.1 异或
0.1.1 用异或做交换
void swap(int& a, int& b){
a = a ^ b;
b = a ^ b;
a = a ^ b;
}
- 原理:
- N ^ N == 0
- N ^ 0 == N
- (A ^ B) ^ C == A ^ (B ^ C)
- A ^ B == B ^ A
- 所以:
- 当:a = a ^ b
- 则:b = a ^ b = a ^ b ^ b = a
- 则:a = a ^ b ^ a = b
- 注意,a和b不可以是同一块内存
0.1.2 用异或找数
题目:1)一个整数数组,只有一个数字出现了奇数次,其他数字都出现了偶数次,找出这个数
int find(vector<int>& arr){
int res = 0;
for(int i = 0; i < arr.size(); i++) res ^= arr[i];
return res;
}
题目:2)一个整数数组,只有两个个数字出现了奇数次,其他数字都出现了偶数次,找出这个数
vector<int>& find(vector<int>& arr){
int res1 = 0;
for(int i = 0; i < arr.size(); i++) res1 ^= arr[i];
int m = res1 & (~res1 + 1);//提取二进制数最右侧的1,这一位俩个数不一样
int res2 = 0;
for(int i = 0; i < arr.size(); i++){
if((arr[i] & m)==0) res2 ^= arr[i];//找到了其中一个数
}
res1 = res1 ^ res2;//根据这一个数和两者的异或,得到另一个数
return {res1,res2};
}
1.排序算法
1.1 简单排序
1.1.1 选择排序
- 时间O(N2) 、空间O(1)、不稳
void SelectionSort(vector<int>& data){
for(int i = 0; i< data.size() - 1; i++){
int min = i;
for(int j = i + 1; j < data.size(); j++){
if(data[j] < data[min]) min = j;
}
swap(data,i,min);
}
}
1.1.2 冒泡排序
- 时间O(N2) 、空间O(1)、稳
void BubbleSort(vector<int>& data){
for(int i = data.size() - 1; i > 0; i--){
for(int j = 0; j < i; j++){
if(data[j] > data[j+1]) swap(data,j,j+1);
}
}
}
1.1.3 插入排序
- 时间O(N2) 、空间O(1)、稳
- 样本数少(<60)一般使用插入排序
void InsertionSort(vector<int>& data){
for(int i = 1; i < data.size(); i++){
for(int j = i; j > 0 && data[j] < data[j-1]; j--){
swap(data,j,j-1);
}
}
}
1.2 特殊排序
1.2.1 归并排序
- 时间O(NlogN)、空间O(N)、稳
- 升级版本是TimSort
void MergeSort(vector<int>& data){
Split(data,0,data.size()-1);
}
void Split(vector<int>& data, int leftPos, int rightPos){
if(leftPos == rightPos) return;
int midPos = 1 + leftPos + (rightPos - leftPos) / 2;
Split(data,leftPos,midPos-1);
Split(data,midPos,rightPos);
Merge(data,leftPos,midPos,rightPos);
}
void Merge(vector<int>& data, int leftPtr, int rightPtr, int endPos){
vector<int> res(endPos - leftPtr + 1);
int i = leftPtr; int j = rightPtr; int k = 0;
while(i <= rightPtr-1 && j <= endPos) res[k++] = data[i] <= data[j] ? data[i++] : data[j++];
while(i <= rightPtr-1) res[k++] = data[i++];
while(j <= endPos) res[k++] = data[j++];
for(int m = 0; m < res.size(); m++) data[leftPtr+m] = res[m];
}
1.2.2 快速排序
- 时间O(NlogN) 、空间O(logN)、不稳
- 升级版是:双轴快排
void QuickSort(vector<int>& data){
Split(data,0,data.size()-1);
}
void Split(vector<int>& data, int leftPos, int rightPos){
if(leftPos >= rightPos) return;
int midPos = Partition(data,leftPos,rightPos);
Split(data,leftPos,midPos-1);
Split(data,midPos+1,rightPos);
}
int Partition(vector<int>& data, int leftPos, int rightPos){
int lptr = leftPos; int rptr = rightPos-1; int pivot = data[rightPos];
while(lptr <= rptr){
while(lptr <= rptr && data[lptr] <= pivot) lptr++;
while(lptr <= rptr && data[rptr] > pivot) rptr--;
if(lptr < rptr) swap(data,lptr,rptr);
}
swap(data,lptr,rightPos);
return lptr;
}
1.2.3 希尔排序
- 时间O(N1.3) 、空间O(1)、不稳
void ShellSort(vector<int>& data){
for(int gap = data.size()/2; gap > 0; gap /= 2)
{
for(int i = gap; i < data.size(); i++)
{
for(int j = i; j > gap-1 && data[j] < data[j-gap]; j -= gap){
swap(data,j,j-gap);}
}
}
}
1.3 桶排序
1.3.1 计数排序
时间O(N+K)、空间O(N+K)、稳
void CountingSort(vector<int>& data)
{
int min = 0; int max = 0;
for(int i = 0; i < data.size(); i++){
if(data[i] > max) max = data[i];
if(data[i] < min) min = data[i];
}
vector<int> count(max - min);
vector<int> res(data.size());
for(int i = 0; i < data.size(); i++) count[data[i]-min]++;
for(int i = 1; i < count.size(); i++) count[i] += count[i-1];
for(int i = data.size()-1; i >= 0; i--) res[--count[data[i]-min]] = data[i];
for(int i = 0; i < data.size(); i++) data[i] = res[i];
}
1.3.2 基数排序
时间O(N*K)、空间O(N)、稳定
void RadixSort(vector<int>& data){
int max = 0,times = 0;
for (int i = 0; i < data.size(); i++) if (data[i] > max) max = data[i];
for (; max > 0; max /= 10) times++;
for (int i = 0; i < times; i++)
{
vector<int> res(data.size());
int count[10] = {};
int dev = pow(10, i);
for (int j = 0; j < data.size(); j++) count[data[j] / dev % 10]++;
for (int j = 1; j < 10; j++) count[j] += count[j - 1];
for (int j = data.size() - 1; j >= 0; j--){
res[--count[data[j] / dev % 10]] = data[j];}
for (int j = 0; j < data.size(); j++) data[j] = res[j];
}
}
1.4 堆排序
- 时间O(NlogN)、空间O(1)、不稳
void heapInsert(vector<int>& arr, int index)
{
while (arr[index] > arr[(index - 1) / 2])
{
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
void heapify(vector<int>& arr, int index, int heapSize)
{
int leftChild = 2 * index + 1;
while (leftChild < heapSize)
{
int large = leftChild + 1 < heapSize && arr[leftChild + 1] > arr[leftChild] ? leftChild + 1 : leftChild;
large = arr[large] > arr[index] ? large : index;
if (large == index) break;
swap(arr, large, index);
index = large;
leftChild = 2 * index + 1;
}
}
void HeapSort(vector<int>& arr)
{
for (int i = 0; i < arr.size(); i++) heapInsert(arr, i);
int heapSize = arr.size();
swap(arr, 0, --heapSize);
while (heapSize > 0)
{
heapify(arr, 0, heapSize);
swap(arr, 0, --heapSize);
}
}
1.5 题目
1.5.1 小和问题 – 改写归并排序
public static int smallSum(int[] arr) {
if (arr == null || arr.length < 2) {
return 0;
}
return mergeSort(arr, 0, arr.length - 1);
}
public static int mergeSort(int[] arr, int l, int r) {
if (l == r) {
return 0;
}
int mid = l + ((r - l) >> 1);
return mergeSort(arr, l, mid)
+ mergeSort(arr, mid + 1, r)
+ merge(arr, l, mid, r);
}
public static int merge(int[] arr, int l, int m, int r) {
int[] help = new int[r - l + 1];
int i = 0;
int p1 = l;
int p2 = m + 1;
int res = 0;
while (p1 <= m && p2 <= r) {
res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0;
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= m) {
help[i++] = arr[p1++];
}
while (p2 <= r) {
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++) {
arr[l + i] = help[i];
}
return res;
}
1.5.2 对基本有序数组排序 – 改写堆排序
- 每个数字都没有移动超过k步
public void sortedArrDistanceLessK(int[] arr, int k) {
PriorityQueue<Integer> heap = new PriorityQueue<>();
int index = 0;
for (; index < Math.min(arr.length, k); index++) {
heap.add(arr[index]);
}
int i = 0;
for (; index < arr.length; i++, index++) {
heap.add(arr[index]);
arr[i] = heap.poll();
}
while (!heap.isEmpty()) {
arr[i++] = heap.poll();
}
}
1.5.3 荷兰国旗问题 – 快排前身
public static int[] partition(int[] arr, int l, int r, int p) {
int less = l - 1;
int more = r + 1;
while (l < more) {
if (arr[l] < p) {
swap(arr, ++less, l++);
} else if (arr[l] > p) {
swap(arr, --more, l);
} else {
l++;
}
}
return new int[] { less + 1, more - 1 };
}
1.5.4 比较器 – 自定义结构排序
public static class Student {
public String name;
public int id;
public int age;
public Student(String name, int id, int age) {
this.name = name;
this.id = id;
this.age = age;
}
}
public static class IdAscendingComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.id - o2.id;
}
}
2. 链表
2.1 基本概念
- 哈希表:
- UnorderdMap – Key-Value
- UnorderdSet – value
- 主要方法:add、remove、put、get – 时间均为O(1)
- 有序表:
- OrderdMap – Key-Value
- OrderdSet – value
- 主要方法:put、get、firstkey、lastkey、floorkey、ceillingkey、remove – 时间为O(logN)
- 基础类型按照值传递、自定义类型引用传递
HashSet<Integer> hashSet1 = new HashSet<>();
hashSet1.add(3);
hashSet1.remove(3);
hashSet1.contains(3)
HashMap<String, Integer> hashMap1 = new HashMap<>();
hashMap1.put(str1, 1);
hashMap1.containsKey(str1);
hashMap1.get(str1);
TreeMap<Integer, String> treeMap1 = new TreeMap<>();
treeMap1.put(7, "我是7");
treeMap1.containsKey(5);
treeMap1.get(5);
treeMap1.firstKey();
treeMap1.lastKey();
treeMap1.floorKey(8);
treeMap1.ceilingKey(8);
treeMap1.remove(5);
- 有序表中存放自定义数据类型时,还需要提供比较器
public class Node{
public int value;
public Node next;
}//单链表
public class Node{
public int value;
public Node next;
public Node last;
}//双链表
- 基本的解题思路:
- 笔试的时候,找时间复杂度最低的方法,借助额外的数据结构来解题
- 面试的时候,还需要空间复杂度尽可能低,尽可能不借助额外的数据结构来解题
- 技巧:使用辅助数据结构、使用快慢指针
2.2 快慢指针方法
快指针 F 和慢指针 S 都从头开始,但 F 一次走两步,S 一次走一步
当 F 到达null的时候,S 到达中点
2.3 题目
2.3.1 最基本的题目
- 反转单链表、双链表
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;
}
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;
}
- 打印链表
public static void printLinkedList(Node head) {// 单链表打印
System.out.print("Linked List: ");
while (head != null) {
System.out.print(head.value + " ");
head = head.next;
}
System.out.println();
}
public static void printDoubleLinkedList(DoubleNode head) {// 双链表打印
System.out.print("Double Linked List: ");
DoubleNode end = null;
while (head != null) {// 正向打印
System.out.print(head.value + " ");
end = head;
head = head.next;
}
System.out.print("| ");
while (end != null) {// 反向打印
System.out.print(end.value + " ");
end = end.last;
}
System.out.println();
}
- 打印两个有序链表的公共部分(归并的思路)
public static void printCommonPart(Node head1, Node head2) {
System.out.print("Common Part: ");
while (head1 != null && head2 != null) {
if (head1.value < head2.value) {
head1 = head1.next;
} else if (head1.value > head2.value) {
head2 = head2.next;
} else {
System.out.print(head1.value + " ");
head1 = head1.next;
head2 = head2.next;
}
}
System.out.println();
}
2.3.2 回文链表
如1->2->2->1为回文列表
- 方法一:
利用栈,将结点依次放入栈中,再挨个弹出与原列表逐个比较
public static boolean isPalindrome1(Node head) { // space O(N)
Stack<Node> stack = new Stack<Node>();
Node cur = head;
while (cur != null) {
stack.push(cur);
cur = cur.next;
}
while (head != null) {
if (head.value != stack.pop().value) {
return false;
}
head = head.next;
}
return true;
}
public static boolean isPalindrome2(Node head) {// space O(N/2)
if (head == null || head.next == null) {
return true;
}
Node right = head.next;
Node cur = head;
while (cur.next != null && cur.next.next != null) {
right = right.next;//慢指针
cur = cur.next.next;//快指针
}
Stack<Node> stack = new Stack<Node>();
while (right != null) {//从中点开始压栈
stack.push(right);
right = right.next;
}
while (!stack.isEmpty()) {//从栈顶一个一个弹出和链表从头部开始依次比较
if (head.value != stack.pop().value) {
return false;
}
head = head.next;//遍历链表
}
return true;
}
- 方法二:
利用快慢指针找到中点,将后半截反序,两个指针从两边遍历到中点,逐个比较
public static boolean isPalindrome3(Node head) {// space O(1)
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;
}
2.3.3 单链表荷兰国旗问题
题目:给定一个单链表,一个Pivot,将链表重新排列为:小于Pivot、等于Pivot、大于Pivot三个区域
- 方法一:
建立一个结点数组,用Partition的方法来对数组里的节点重新排列,最后连接起来
public static Node listPartition1(Node head, int pivot) {
if (head == null) {
return head;
}
Node cur = head;
int i = 0;
while (cur != null) {// 计算结点个数
i++;
cur = cur.next;
}
Node[] nodeArr = new Node[i];// 用结点个数来建立新数组
i = 0;
cur = head;
for (i = 0; i != nodeArr.length; i++) {// 将结点依次填入数组中
nodeArr[i] = cur;
cur = cur.next;
}
arrPartition(nodeArr, pivot);// 对数组进行荷兰国旗问题处理
for (i = 1; i != nodeArr.length; i++) {// 对排列好的结点依次连接成链表
nodeArr[i - 1].next = nodeArr[i];
}
nodeArr[i - 1].next = null;
return nodeArr[0];
}
public static void arrPartition(Node[] nodeArr, int pivot) {// 对数组进行荷兰国旗问题处理
int small = -1;
int big = nodeArr.length;
int index = 0;
while (index != big) {
if (nodeArr[index].value < pivot) {
swap(nodeArr, ++small, index++);
} else if (nodeArr[index].value == pivot) {
index++;
} else {
swap(nodeArr, --big, index);
}
}
}
- 方法二:
使用六个指针:SH、ST、EH、ET、BH、BT 分别指向 小于区域、等于区域、大于区域的头和尾
一开始六个指针都指向null,
发现第一个满足区域条件时,该区域的H指针指向它就不动了
每发现一个满足该区域条件时,该区域的T指针指向它,该区域的各个节点相连
最后:ST->EH、ET->BH,而SH为新链表的头
要注意边界,因为某个区域可能为空
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; // 断开连接
// H如果为空,H就要等于head,
// 否则让尾端的下一个结点连head,
// 不管怎么样,在最后都要让T等于head
if (head.value < pivot) { // 小于区域
if (sH == null) sH = head;
else sT.next = head;
sT = head;
}
else if (head.value == pivot) { // 等于区域
if (eH == null) eH = head;
else eT.next = head;
eT = head;
}
else { // 大于区域
if (bH == null) bH = 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;
}
2.3.4 复制特殊链表
题目:一种新的链表,除了有Node next成员外,还有一个Node rand随机指向一个节点,将该链表复制一遍
- 方法一:
用Map:key-value的查找方法,挨个对应
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);
}
- 方法二:
在每个节点后面复制一个该节点,然后遍历,一一对应rand指针
然后逐个解开next,对应新next
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 复制品的rand挨个连接对应复制品 cur.rand.next
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 复制品和原链表分离的同时,将复制品的next连对
while (cur != null) {
next = cur.next.next;
curCopy = cur.next;
cur.next = next;
curCopy.next = next != null ? next.next : null;
cur = next;
}
return res;
}
*2.3.5 一个单链表是否有环
上面为无环,下面为有环,B为入环节点
- 方法一:
用set来依次放入节点,当第一次查找能找到时,这个节点就是入环节点
- *方法二:改进的快慢指针
S 和 F 从头节点出发,若 F 到了null,则一定无环
若 S 和 F 相遇,则一定有环
此时,让 F 回到起点,S 和 F 一次走一步,它们一定会在入环节点相遇
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;// 返回相遇的点
}
2.3.6 两个单链表相交
题目:两个单链表head1和head2,可能有环,判断它们相交与否,若相交,返回第一个相交结点
先判断是不是有环:
- 都无环,有两种情况
找这两个链表的end,即遍历到null,同时记录两个链表的长度len1、len2
若null前最后一个节点不同,则说明不相交,若相同则说明相交
记n = len1 - len2,令一个指针怕P1从较长的链表的头部出发,先走n步,
然后另一个指针P2从较短链表头部和P1一起出发,它们必然会在第一个相交结点相遇
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) {// 遍历表1,得到表一长度n、尾端结点cur1
n++;
cur1 = cur1.next;
}
while (cur2.next != null) {// 遍历表2,得到两表长度之差n、尾端结点cur2
n--;
cur2 = cur2.next;
}
if (cur1 != cur2) {// 尾端结点不一致,直接判断没有交点
return null;
}
cur1 = n > 0 ? head1 : head2;// 复用两个指针,cur1为较长的链表的头
cur2 = cur1 == head1 ? head2 : head1;// cur2为较短的链表的头
n = Math.abs(n);
while (n != 0) {// cur1从较长链表头先走长度差n步
n--;
cur1 = cur1.next;
}
while (cur1 != cur2) {// 两者一起走,直到相遇
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;// 返回相遇的点
}
-
一个有环另一个无环,必然无交点
-
两个都有环,三种情况
这个时候会返回两个入环节点:loop1和loop2
若loop1 == loop2,则如下图,则相交结点在入环节点之前,用1.里的方法可以得到b
让loop1往后走,转一圈到自己,若没有遇到loop2,如下图,两链表必然不相交
若遇到了loop2,如下图所示,则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) {// 让一个指针从loop1点起步再转回loop1
if (cur1 == loop2) {// 如果指针遇到了loop2,说明是情况三
return loop1;// 就返回loop1
}
cur1 = cur1.next;
}
return null;// 否则说明是第二种情况,无交点
}
}
然后调用这两个函数即可:
public static Node getIntersectNode(Node head1, Node head2) {
if (head1 == null || head2 == null) {
return null;
}
Node loop1 = getLoopNode(head1);// 判断两个链表是否有环,并返回入环结点
Node loop2 = getLoopNode(head2);
if (loop1 == null && loop2 == null) {//两个都没环
return noLoop(head1, head2);
}
if (loop1 != null && loop2 != null) {//两个都有环
return bothLoop(head1, loop1, head2, loop2);
}
return null;//一个有环一个没环不可能相交
}
3. 二叉树
3.1基本结构
public class Node{
public int value;
public Node left;
public Node right;
}
一颗二叉树:
3.2 二叉树的基本遍历
3.2.1 递归遍历
public static void f(Node.head){
if(head == null) return;
//1.打印
f(head.left);
//2.打印
f(head.right);
//3.打印
}
1、2、3处都打印的话,结果为:(递归序)
1,2,4,4,4,2,5,5,5,2,1,3,6,6,6,3,7,7,7,3,1
只在1.处打印:(先序遍历–深度优先遍历–头、左、右)preOrder
1,2,4,5,3,6,7
只在2.处打印:(中序遍历–左、右、头)inOrder
4,2,5,1,6,3,7
只在3.处打印:(后续遍历–左、右、头)posOrder
4,5,2,6,7,3,1
3.2.2 非递归遍历 – 栈
- 先序:
弹出一个结点 N --> 打印 --> 放入N左、放入N右 --> 循环
public static void preOrderUnRecur(Node head) {
System.out.print("pre-order: ");
if (head != null) {
Stack<Node> stack = new Stack<Node>();
stack.add(head); // 头节点入栈
while (!stack.isEmpty()) {
head = stack.pop(); // 弹出一个结点head
System.out.print(head.value + " "); // 立刻打印
if (head.right != null) {// 依次放入head的右孩子
stack.push(head.right);
}
if (head.left != null) {// 和左孩子
stack.push(head.left);
}
}
}
System.out.println();
}
- 后序:(两个栈)
结点N从栈A出来 --> 放入栈B --> N左入栈A、N右入栈A -->循环
最后从栈B依次弹出打印
public static void posOrderUnRecur1(Node head) {
System.out.print("pos-order: ");
if (head != null) {
Stack<Node> s1 = new Stack<Node>();// 建立两个栈,栈1
Stack<Node> s2 = new Stack<Node>();// 和栈2
s1.push(head);
while (!s1.isEmpty()) {
head = s1.pop();// 栈1弹出一个结点head
s2.push(head);// head放入栈2
if (head.left != null) {// 依次在栈1中放入head的左孩子
s1.push(head.left);
}
if (head.right != null) {// 和右孩子
s1.push(head.right);
}
}
while (!s2.isEmpty()) {// 栈2依次弹出并打印
System.out.print(s2.pop().value + " ");
}
}
System.out.println();
}
- 中序:
每棵子树,整棵树左边界依次进栈,依次弹出,处理,对右树循环
public static void inOrderUnRecur(Node head) {
System.out.print("in-order: ");
if (head != null) {
Stack<Node> stack = new Stack<Node>();
while (!stack.isEmpty() || head != null) {
if (head != null) {// 将一个结点放入栈中,并继续处理其左孩子
stack.push(head);
head = head.left;
} else {// 没有左孩子则对右孩子做处理
head = stack.pop();
System.out.print(head.value + " ");
head = head.right;
}
}
}
System.out.println();
}
3.2.3 保留格式打印二叉树
public static void printTree(Node head) {
System.out.println("Binary Tree:");
printInOrder(head, 0, "H", 17);
System.out.println();
}
public static void printInOrder(Node head, int height, String to, int len) {
if (head == null) {
return;
}
printInOrder(head.right, height + 1, "v", len);
String val = to + head.value + to;
int lenM = val.length();
int lenL = (len - lenM) / 2;
int lenR = len - lenM - lenL;
val = getSpace(lenL) + val + getSpace(lenR);
System.out.println(getSpace(height * len) + val);
printInOrder(head.left, height + 1, "^", len);
}
public static String getSpace(int num) {
String space = " ";
StringBuffer buf = new StringBuffer("");
for (int i = 0; i < num; i++) {
buf.append(space);
}
return buf.toString();
}
3.3 宽度优先遍历
3.3.1 队列方法遍历
- 队列头弹出一个节点 N
- 队列尾依次放入 N左 和 N右
- 循环
public static void w(Node head){
if(head == null) return;
Queue<Node> queue = new LinkedList<>();
queue.add(head);
while(!queue.isEmpty()){
Node cur = queue.poll();//弹出队头节点
System.out.println(cur.value);
if(cur.left != null) queue.add(cur.left);
if(cur.right != null) queue.add(cur.right);
}
}
3.3.2 计算二叉树最大宽度
- 要求:计算宽度需要知道每个节点所在层数,并统计每层的节点个数
- 利用哈希表,在每个节点入队列时,哈希表中记录节点和节点所在层数
public static int w(Node head){
if(head == null) return;
Queue<Node> queue = new LinkedList<>();
queue.add(head);
= HashMap<Node,Integer> levelMap = new HashMap<>();
= levelMap.put(head,1);
= int curLevel = 1;//当前在哪一层
= int curLevelNodes = 0;//当前层节点个数
= int max = Integer.Min_Value;
while(!queue.isEmpty()){
Node cur = queue.poll();
= int curNodeLevel = levelMap.get(cur);
= if(curNodeLevel == curLevel) curLecelNodes++;
= else {
= max = Math.max(max,curLevelNodes);
= curLevel++;
= curLevelNodes = 1;
= }
System.out.println(cur.value);
if(cur.left != null) {
= levelMap.put(cur.left, curNodeLevel+1);
queue.add(cur.left);
}
if(cur.right != null){
= levelMap.put(cur.right, curNodeLevel+1);
queue.add(cur.right);
}
}
return max;
}
- 不用哈希表的方法:
- 当前层最后一个节点:
curend
- 下一层最后一个变量:
nextend
(始终为最后进栈的节点) - 当前层节点个数:
curlevelnodes
- 当前层最后一个节点:
3.4 二叉树相关概念判断
3.4.1 搜索二叉树-BST
-
概念:所有子树,左节点 < 头节点 < 右节点
-
中序遍历,若结果是升序<=>搜索二叉树
//全局变量用来比较
public static int preValue = Integer.MIN_VALUE;
public static boolean isBST(Node head){
//到达叶子
if(head == null) return true;
//判断左树
if(!isBST(Node.left)) return false;
//更新全局变量
if(head.value <= preValue) return false;
else preValue = head.value;
//判断右树
return isBST(Node.right);
}
//另一种方法:中序遍历放入列表中,然后遍历列表,确认为升序则为BST
//非递归方法:
public static boolean isBST2(Node head){
if(head != null){
//设置外部变量用于比较
int preValue2 = Integer.MIN_VALUE;
//用栈来实现非递归
Stack<Node> stack = new Stack<Node>();
while(!stack.isEmpty() || head != null){
//一路向左直到叶子
if(head != null){
stack.push(head);
head = head.left;
}
else{
head = stack.pop();//反序弹出左树
//更新外部变量
if(head.value <= preValue2) return false;
else preValue2 = head.value;
//检查右树
head = head.right;
}
}
}
}
//套路方法:每个节点需要的信息:
//左树是否为BST、左树的max
//右树是否为BST、右树的min
//根据上述需求定义结构体/类
public static class ReturnData{
public boolean isBST;
public int min;
public int max;
public ReturnData(boolean is, int mi, int ma){
isBST = is;
min = mi;
max = ma;
}
}
//process 方法
public static ReturnData process(Node X){
if(x == null) return null;//空树返回null
ReturnData leftData = process(x.left); //左子树递归
ReturnData rightData = process(x.right); //右子树递归
//求x节点的三个信息
boolean isBST; int min = x.value; int max = x.value;
if(leftData!=null){
min = Math.min(min,leftDta.min);
max = Math.max(min,leftDta.max);
}
if(rightData!=null){
min = Math.min(min,rightData.min);
max = Math.max(min,rightData.max);
}
? boolean isBST = true;
? if(leftData!=null && (!leftData.isBST || leftData.max >= x.value)) isBST = false;
? if(rightData!=null && (!rightData.isBST || rightData.min <= x.value)) isBST = false;
return new ReturnData(isBST, min, max);
}
//前面为?的三行可以换为:
boolean isBST = false;
if( (leftData != null ? (leftData.isBST && leftData.max < x.value) : true)
&&
(rightData != null ? (rightData.isBST && rightData.min > x.value) : true)
) isBST = true;
3.4.2 完全二叉树-CBT
-
大多不能用套路
-
宽度优先遍历,①任一节点若有右节点无左节点,直接
false
-
在在①不违规的条件下,第一个左右子不全,后续皆是叶节点则为二叉树,否则不是完全二叉树
public static boolean isCBT(Node head){
if(head == null) return true;
LinkedList<Node> queue = new LinkedList<>();
= boolean leaf = false;
node L = null;
node R = null;
queue.add(head);
while(!queue.isEmpty()){
head = queue.poll();
L = head.left;
R = head.right;
= if( (leaf && (L != null || R != null)) //遇到过不双全 且 又遇到了
= ||(L == null && R != null) ) return false; //有右无左
if(L != null) queue.add(L);
if(R != null) queue.add(R);
= if(L == null || r == null) leaf = ture;//遇到了有左无右 或 俩都没有
}
return true;
}
//去掉=行,则为宽度优先遍历代码
3.4.3 满二叉树-F
- 方法一:遍历树,统计深度H、节点数N,满足N = 2H - 1 则为满二叉树(充分必要条件)
- 方法二:用套路来遍历树:
public static class Info{
public int height;
public int nodes;
public Info(int h, int n){
height = h;
nodes = n;
}
}
public static Info process(Node x){
if(x == null) return new Info(0,0);
Info leftData = process(x.left);
Info rightData = process(x.right);
int height = Math.max(leftData.height, rightData.height)+1;
int nodes = leftData.nodes + rightData.nodes + 1;
return new Info(height,nodes);
}
public static boolean isF(Node head){
if(head == null) return true;
Info data = process(head);
return data.nodes == 1 << data.height -1;
}
*3.4.4 平衡二叉树-BalancedTree
-
平衡二叉树:每颗二叉树的__左树__和__右树__的高度差都不能超过1
- 左树、右树均为平衡二叉树
- 且|左树高度 - 右树高度|<=1
-
因此每个子树都需要 【高度、是否是平衡树】的结构体信息
public static class ReturnType{
public boolean isBalanced;
public int height;
public ReturnType(boolean isB,int hei){
isBalanced = isB;
height = hei
}
}//定义一个结构体/类
public static ReturnType process(Node x){
if(x == null) return new ReturnType(true,0); //空树必为平衡树
ReturnType leftData = process(x.left); //左子树递归
ReturnType rightData = process(x.right); //右子树递归
int height = Math.max(leftData.height,right.height)+1; //求得该树高度
boolean isBalanced = leftData.isBalanced && rightData.isBalanced
&& Math.abs(leftData.height - rightData.height) < 2;
return new ReturnType(isBalanced, height);
}
public static boolean isBalanced(Node head){ //主函数
return process(head).isBalaced;
}
二叉树题目套路 – 树形动态规划(DP)套路:
- 递归,假设一个节点可以得到它:左子树和右子树的所有信息
- 筛选必须要得到的信息,创建结构体Struct1
- 写一个返回值为Struct1类型的process函数用于递归
- 用__左右两个子树__的信息来得到需要返回的结构体的所有信息,实现一个闭环
3.5 题目
3.5.1 最低公共祖先节点
-
题目:给定两个二叉树的节点node1和node2,找到他们的最低公共祖先节点
-
方法一:
public static Node LCA(Node head, Node o1, Node o2){
HashMap<Node, Node> fatherMap = new HashMap<>();
fatherMap.put(head, head);
process(head, fatherMap);
HashSet<Node> set1 = new HashSet<>();
Node cur = o1;
while(cur != fatherMap.get(cur)){
set1.add(o1);
cur = fatherMap.get(cur);
}
set1.add(head);
cur = o2;
while(cur != fatherMap.get(cur)){
if(!set1.contains(cur)) cur = fatherMap.get(cur);
else return cur;
}
}
public static void process(Node head, HashMap<Node, Node> fatherMap){
if(head == null) return;
fatherMap.put(head.left, head);
fatherMap.put(head.right, head);
process(head.left, fatherMap);
process(head.right, fatherMap);
}
方法二:
public static Node LCA(Node head, Node o1, Node o2){
if(head ==null || head == o1 || head == o2) return head;
Node left = LCA(head.left, o1, o2);
Node right = LCA(head.right, o1, o2);
if(left != null && right != null) return head;
return left != null ? left : right;
}
- 两种结构:
- o1是o2的LCA,或o2是o1的LCA
- o1与o2不互为LCA,不断向上找到
3.5.2 后继节点
- B就是D的后继节点,B的后继节点是E
- 题目:如果节点中有指向父节点的指针,理论上只需要走层数距离就可以找到后继节点
分析:
- 如果x有右树,那么其后继为其右树的最左节点
- x无右树
- 若x是父节点的左节点,则父节点就是其后继
- 若x是父节点的右节点,则x父亲是不是其父亲的左节点,循环
public static Node getLeftMost(Node node){
if(node == null) return node;
while(node.left != null) node = node.left;
return node;
}//一路向左
public static Node getSuNode(Node node){
if(node == null) return node;
if(node.right != null) return getLeftMost(node.right);
else{
Node parent = node.parent;
while(parent != null && parent.left != node){
node = parent;
parent = node.parent;
}//一路向上
return parent;
}
}
3.5.3 二叉树的序列化和反序列化
- 题目:一棵树变成字符串,字符串在变成该树,一一对应
方法一:先序方法
public static String serialByPre(Node head){
if(head == null) return "#_";
String res = head.value + “_”;
res += serialByPre(head.left);
res -= serialBypre(head.right);
return res;
}
public static Node reconPreOrder(Queue<String> queue){
String value = queue.poll();
if(value.equals('#')) return null;
Node head = new Node(Integer.valueOf(value));
head.left = reconPreOrder(queue);
head.right = reconpreOrder(queue);
return head;
}
public static Node reconByPreString(String preStr){
String [] values = preStr.split("_");
Queue<String> queue = new LinkedList<String>();
for(int i = 0; i != values.length; i++) queue.add(values[i]);
return reconPreOrder(queue);
}
3.5.4 折纸问题
- 题目:凹折痕、凸折痕,给出对折次数,从上到下打印折痕方向
满二叉树:左孩子都是凹,右孩子都是凸,折叠次数为层数 ——> 中序遍历
public static void printAllFolds(int N){
printProcess(1,N,true);
}
public static void printProcess(int i, int N, boolean down){
if(i > N) return;
printProcess(i + 1, N, true);//一路向左到底
System.out.println(down ? "v" : "^");//回来到这的时候打印
printProcess(i + 1, N, false);//遍历右树
}
4. 图
图的算法都很简单,复杂的是图本身的表达
4.1 图的表达方式
- 两个要素:点集、边集
4.1.1 邻接表法:
A: C, D
B: C
C: A, B, D
D: A, C
4.1.2 邻接矩阵
A | B | C | D | |
---|---|---|---|---|
A | 0 | inf | w | w |
B | inf | 0 | w | inf |
C | w | w | 0 | w |
D | w | inf | w | 0 |
掌握一种结构,用这一种结构把所有算法实现一次,后续只需要写其他结构的接口
public class Graph{
public HashMap<Integer, Node> nodes;//编号+点
public HashMap<Edge> edges;
public Graph(){
nodes = new HashMap<>();
edges = new HashMap<>();
}
}
public class Node{
public int value; //值
public int in; //入度
public int out; //出度
public ArrayList<Node> nexts; //该点出发的所有点(对应出度)
public ArrayList<Edge> edges; //该店出发的所有边(对应出度)
public Node(int val){
value = val;
in = 0;
out = 0;
nexts = new ArrayList<>();
edges = new ArrayList<>();
}
}
public class Edge{
public int weight;
public Node from;
public Node to;
public Edge(int wei, Node fro, Node t){
weight = wei;
from = fro;
to = t;
}
}
如:由 [weight,from,to] 转化为上述结构:
public static Graph createGraph(Integer[][] matrix){
Graph graph = new Graph();
for(int i = 0; i < matrix.length; i++){
Integer weight = matrix[i][0];
Integer from = matrix[i][1];
Integer to = matrix[i][2];
if(!graph.nodes.contaninsKey(from)) graph.nodes.put(from, new Node(from));
if(!graph.nodes.contaninsKey(to)) graph.nodes.put(from, new Node(to));
Node fromNode = graph.nodes.get(from);
Node toNode = graph.nodes.get(to);
Edge newEdge = new Edge(weight, fromNode, toNode);
fromNode.next.add(toNode);
fromNode.out++;
toNode.in++;
fromNode.edges.add(newEdge);
graph.edges.add(newEdge);
}
return graph;
}
4.2 图的遍历
4.2.1 宽度优先遍历–队列–BFS
public static void BFS(Node node){
if(node == null) return;
Queue<Node> queue = new LinkedList<>();
HashSet<Node> set = new HashSet<>();//用来查重
queue.add(node);
set.add(node);
while(!queue.isEmpty()){
Node cur = queue.poll();
System.out.println(cur.value);
for(Node next : cur.next){
if(!set.contains(next)){
set.add(next);
queue.add(next);
}
}
}
}
- 如果这个图是有编号的,直接使用arr[ ] 0~1000之类的,和哈希表等效但更快
4.2.2 深度优先遍历–栈--DFS
public static void DFS(Node node){
if(node == null) return;
Stack<Node> stack = new Stack<>();
HashSet<Node> set = new HashSet<>();
stack.add(node);
set.add(node);
System.out.println(node.value);
while(!stack.isEmpty()){
Node cur = stack.pop();//最后查重
for(Node next : cur.nexts){
if(!set.contains(next)){
stack.push(cur);
stack.push(next);
set.add(next);
System.out.println(next.value);
break;//跳出for循环,只看cur的一个邻居
}
}
}
}
- 栈里为深度优先的路径
4.4 拓扑排序-Topology
先找入度为零的点(A),它一定为起点,接下来去掉和A有关的边
会有第二个入度为零的点(B),它就排在A后面,接下来去掉和B有关的所有的边
以此类推,每次找入度为0的点,然后去掉该点。由此得到A->B->C->D
public static List<Node> sortedTopology(Graph graph){
HashMap<Node, Integer> inMap = new HashMap<>();
Queue<Node> zeroInQueue = new LinkedList<>();
for(Node node : graph.nodes.values()){
inMap.put(node, node.in);//记录所有入度
if(node.in == 0) zeroInQueue.add(node);
}
List<Node> result = new ArrayList<>();
while(!zeroInQueue.isEmpty()){
inMap.put(next, inMap.get(next) - 1);//擦除上一个点有关的所有入度
if(inMap.get(next) == 0) zeroInQueue.add(next);
}
return result;
}
4.5 最小生成树-MST
将无向图变成一个树,要求权值和最小
4.5.1 Kruskal算法–从边的角度
把所有边按weight排序,挨个加上,看有没有形成环,形成环则不要该边
如何判断形成环?
- 将每个点设置为一个集合,如果使用了一条边,看头和尾所在的集合是否为一个集合
- 若为一个集合,则说明形成了环,这条边不能要
- 若不是一个集合,则说明没有形成环,合并这两个集合
public static class MySets{
public HashMap<Node, List<Node>> setMap;
public MySet(List<Node> nodes){
for(Node cur : nodes){
List<Node> set = new ArrayList<Node>();
set.add(cur);
setMap.put(cur,set);
}
}
public boolean isSameSet(Node from, Node to){
List<Node> fromSet = setMap.get(from);
List<Node> toSet = setMap.get(to);
return fromSet == toSet;
}
public void union(Node from, Node to){
List<Node> fromSet = setMap.get(from);
List<Node> toSet = setMap.get(to);
for(Node toNode : toSet){
fromSet.add(toNode);
setMap.put(toNode, fromSet);//修改fromset
}
}
}
//使用并查集的K算法
public static Set<Edge> krukalMST(Graph graph){
UnionFind unionFind = new UnionFind();
UnionFind.makeSets(graph.nodes.values());
PriorityQueue<Edge> priorityQueue = new priorityQueue<>(new EdgeComparator());
for(Edge dege : graph.edges) priorityQueue.add(edge);//用比较器和优先级队列进行排序
Set<Edge> result = new HashSet<>();
while(!priorityQueue.isEmpty()){
Edge edge = priorityQueue.poll();
if(!unionFind.isSameSet(edge.from, edge.to)){
result.add(edge);
unionFind.union(edge.from, edge.to);
}
}
return result;
}
- 几片连在一起
4.5.2 Prim算法–从点的角度
选一个点,然后解锁这个点所有的边,选最小的,然后解锁这条边的另一端点,
解锁这个点的所有边,在所有解锁的边里选最小的(选过的边、两个端点都已经解锁的边不算)
…循环,直到所有的点都完成
public static Set<Edge> primMST(Graph graph){
PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator);
HashSet<Node> set = new HashSet<>();
Set<Edge> result = new HashSet<>();
for(Node node : graph.nodes.value()){//处理森林问题(有可能整个图不是全连通的)
if(!set.contains(node)){
set.add(node);//开始的点
for(Edge edge : node.edges) priorityQueue.add(edge);//加入小顶堆
while(!priorityQueue.isEmpty()){
Edge edge = priorityQueue.poll();//弹出权值最小的边
Node toNode = edge.to;//这条边指向的点
if(!Set.contains(toNode)){//没包含过这个点,这条边可行
set.add(toNode);
result.add(edge);
for(Egde nextEdge : toNode.edges) priorityQueue.add(nextEdge);
//解锁该点的其他边
}
}
}
}
}
- 一大片里一个一个加东西
4.6 最短路径–Dijkstra算法
适用于:不能有累加和为负数的环
规定了出发点,算出从该点出发,到每个点的最短距离
如:A–B--3、A–C--5、A–D--9、A–E--19
A | B | C | D | E | |
---|---|---|---|---|---|
A | 0 | inf | inf | inf | inf |
初始化一个表,从左到右依次取点,这个点之前锁死,以这个点为起点连成的路径若能使表格内数据更小,更新数据
public static HashMap<Node, Integer> dijkstra(Node head){
HashMap<Node, Integer> distanceMap = new HashMap<>();
distanceMap.put(head, 0);
HashSet<Node> selectedNodes = new HashSet<>();//已经锁住的节点
Node minNode = getMinDAndUN(distanceMap, selectedNodes);
while(minNode != null){
int distance = distanceMap = distanceMap.get(minNode);
for(Edge edge : minNode.edges){
Node toNode = edge.to;
if(!distanceMap.containsKey(toNode)) distanceMap.put(toNode, distance + edge.weight);
distanceMap.put(edge.to, Math.min(distanceMap.get(toNode), distance + edge.weight));
}
selectedeNodes.add(minNodes);
minNode = getMinDAndUN(distanceMap, selectedeNodes);
}
return distanceMap;
}
public static Node getMinDAndUN(HashMap<Node, Integer> distanceMap, HashSet<Node> selectedNodes){
Node minNode = null;
int minDistance = Integer.MAX_VALUE;
for(Entry<Node, Integer> entry : distanceMap.entrySet()){
Node node = entry.getKey();
int distance = entry.getValue();
if(!selectedNodes.contains(node) && distance < minDistance){
minNode = node;
minDistance = distance;
}
}
return minNode;
}
- 可以用堆来优化
public static HashMap<Node, Integer> dijkstra2(Node head, int size) {
NodeHeap nodeHeap = new NodeHeap(size);
nodeHeap.addOrUpdateOrIgnore(head, 0);
HashMap<Node, Integer> result = new HashMap<>();
while (!nodeHeap.isEmpty()) {
NodeRecord record = nodeHeap.pop();
Node cur = record.node;
int distance = record.distance;
for (Edge edge : cur.edges) {
nodeHeap.addOrUpdateOrIgnore(edge.to, edge.weight + distance);
}
result.put(cur, distance);
}
return result;
}
public static class NodeHeap {
private Node[] nodes;
private HashMap<Node, Integer> heapIndexMap;
private HashMap<Node, Integer> distanceMap;
private int size;
public NodeHeap(int size) {
nodes = new Node[size];
heapIndexMap = new HashMap<>();
distanceMap = new HashMap<>();
this.size = 0;
}
public boolean isEmpty() {
return size == 0;
}
public void addOrUpdateOrIgnore(Node node, int distance) {
if (inHeap(node)) {
distanceMap.put(node, Math.min(distanceMap.get(node), distance));
insertHeapify(node, heapIndexMap.get(node));
}
if (!isEntered(node)) {
nodes[size] = node;
heapIndexMap.put(node, size);
distanceMap.put(node, distance);
insertHeapify(node, size++);
}
}
public NodeRecord pop() {
NodeRecord nodeRecord = new NodeRecord(nodes[0], distanceMap.get(nodes[0]));
swap(0, size - 1);
heapIndexMap.put(nodes[size - 1], -1);
distanceMap.remove(nodes[size - 1]);
nodes[size - 1] = null;
heapify(0, --size);
return nodeRecord;
}
private void insertHeapify(Node node, int index) {
while (distanceMap.get(nodes[index]) < distanceMap.get(nodes[(index - 1) / 2])) {
swap(index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
private void heapify(int index, int size) {
int left = index * 2 + 1;
while (left < size) {
int smallest = left + 1 < size && distanceMap.get(nodes[left + 1]) < distanceMap.get(nodes[left])
? left + 1 : left;
smallest = distanceMap.get(nodes[smallest]) < distanceMap.get(nodes[index]) ? smallest : index;
if (smallest == index) {
break;
}
swap(smallest, index);
index = smallest;
left = index * 2 + 1;
}
}
private boolean isEntered(Node node) {
return heapIndexMap.containsKey(node);
}
private boolean inHeap(Node node) {
return isEntered(node) && heapIndexMap.get(node) != -1;
}
private void swap(int index1, int index2) {
heapIndexMap.put(nodes[index1], index2);
heapIndexMap.put(nodes[index2], index1);
Node tmp = nodes[index1];
nodes[index1] = nodes[index2];
nodes[index2] = tmp;
}
}
*5. 贪心算法
笔试比面试难
5.1 前缀树
一个字符串类型的数组arr1,另一个字符串类型的数组类型arr2。
arr2中有哪些字符,是arr1中出现的?请打印。
arr2中有哪些字符,是作为arr1中某个字符串前缀出现的?请打印。
请打印arr2中出现次数最大的前缀。
前缀树结点:
public static class TrieNode{
public int pass;
public int end;
public TrieNode[] nexts;//如果不只是英文字母,可以用哈希表<char,node>
public TreNodes(){
pass = 0;
end = 0;
nexts = new TrieNode[26];//0-a,1-b,2-c....
}
}
前缀树结构体:
public static class Trie{
private TrieNode root;
public Trie(){
root = new TrieNode();
}
public void insert(String word){// 插入一个字符
if(word == null) return;
char[] chs = word.toCharArray();
TrieNode node = root;
node.pass++;
int index = 0;
for(int i= 0; i < chs.length; i++){
index = chs[i] - 'a';
if(node.nexts[index] == null) node.nexts[index] = new TrieNode();
node = node.nexts[index];
node.pass++;
}
node.end++;
}
public int search(String word){//寻找该字符加入了几个
if(word == null) return 0;
char[] chs = word.toCharArray();
TrieNode node = root;
int index = 0;
for(int i = 0; i< chs.length; i++){
index = chs[i] - 'a';
if(node.nexts[index] == null) return 0;
node = node.nexts[index];
}
return node,end;
}
public int prefixNumber(String pre){//寻找前缀数量
if(pre == null) return 0;
char[] chs = pre.toCharArray();
TrieNode node = root;
int index = 0;
for(int i = 0; i < chs.length; i++){
index = chs[i] - 'a';
if(node.nexts[index] == null) return 0;
node = node.nexts[index];
}
return node.pass;
}
public void delete(String word){//删除一个字符串
if(search(word) != 0){
char[] chs = word.toCharArray();
TrieNode node = root;
node.pass--;
int index = 0;
for(int i = 0; i < chs.length; i++){
index = chs[i] - 'a';
if(--node.nexts[index].pass == 0){
node.nextsp[index] = null;
//c++还要遍历到底部 手动析构
return;
}
node = node.nexts[index];
}
node.end--;
}
}
}
5.2 贪心算法
在某个标准下,优先考虑最满足标准的样本,最后考虑最不满足标准的样本,最终得到一个答案的算法,叫贪心算法
5.2.1 基本思路
局部最优–>整体最优
5.2.2 解题技巧
贪心算法的在笔试时的解题套路:
- 实现一个不依靠贪心策略的解法X,可以用最暴力的尝试
- 脑补出贪心策略A、贪心策略B、贪心策略C…
- 用解法X和对数器,去验证每一个贪心策略,用实验的方式得知哪个贪心策略正确
- 不要去纠结贪心策略的证明
常用技巧:
- 建立比较器排序
- 建立比较器来构造堆
5.3 题目
5.3.1 会议室安排宣讲会
-
一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目的宣讲。
-
给你每一个项目开始的时间和结束的时间(给你一个数组,里面是一个个具体的项目),你来安排宣讲的日程,要求会议室进行的宣讲的场次最多。
-
返回这个最多的宣讲场次。
分析:按结束时间排序
public static class Program {// 会议
public int start;// 开始时间
public int end;// 结束时间
public Program(int start, int end) {
this.start = start;
this.end = end;
}
}
public static class ProgramComparator implements Comparator<Program> {
@Override
public int compare(Program o1, Program o2) {// 比较器
return o1.end - o2.end;// 按照结束时间排序,早结束的放前面
}
}
public static int bestArrange(Program[] programs, int start) {
Arrays.sort(programs, new ProgramComparator());//排序,早结束的放前面
int result = 0;
for (int i = 0; i < programs.length; i++) {
if (start <= programs[i].start) {// 开始时间早于会议开始时间
result++;
start = programs[i].end;
}
}
return result;
}
5.3.2 最小字典序的字符串拼接
一个字符串在字典里排的越靠前,其字典序越小如:
-
abc < bck
-
b > apple
最朴素的想法:每个串进行排序,然后拼接(这个方案不对)
一个策略:a和b结合,比较b和a结合,谁小谁放前面
public static class MyComparator implements Comparator<String> {
@Override
public int compare(String a, String b) {// 比较器
return (a + b).compareTo(b + a);//ab比较ba
}
}
public static String lowestString(String[] strs) {
if (strs == null || strs.length == 0) {
return "";
}
Arrays.sort(strs, new MyComparator());// 用自定义的比较器排序
String res = "";
for (int i = 0; i < strs.length; i++) {// 按顺序连接起来
res += strs[i];
}
return res;
}
如何证明比较是有效的?
-
比较一定是要有传递性的,如a<b、b<c,那a一定小于c
-
所以只需证:若ab<ba && bc<cb,则ac<ca
- 把字符串理解为K进制的数字
- a拼b == a*m(b)+b,m(b)为Kb长度
- 所以有:
a * m(b) + b <= b * m(a) + a
b * m(c) + c <= c * m(b) + b
- 则有:
a * m(b) * c <= b * m(a) * c + a * c - b * c
b * m(c) * a + c * a - b * a <= c * m(b) * a
- 即:
b * m(c) * a + c * a - b * a <= b * m(a) * c + a * c - b * c
- 进而:
m(c) * a + c <= m(a) * c + a
- 即:
a拼c < c拼a
- 即这种方法排序是有传递性的
-
接下来证明有效性
- 下面的序列字典序是递增的
- […,a,m1,m2,b,…]
- […,m1,a,m2,b,…]
- […,m1,m2,a,b,…]
- […,m1,m2,b,a,…]
- 然后用数学归纳法
5.3.3 切金条–哈夫曼编码问题
题目:一块金条切成两半,是需要花费和长度数值一样的铜板的。
比如长度为20的金条,不管切成长度多大的两半,都要花费20个铜板。
一群人想整分整块金条,怎么分最省铜板?
- 用堆的方法
public static int lessMoney(int[] arr) {
PriorityQueue<Integer> pQ = new PriorityQueue<>();
for (int i = 0; i < arr.length; i++) {
pQ.add(arr[i]);
}
int sum = 0;
int cur = 0;
while (pQ.size() > 1) {
cur = pQ.poll() + pQ.poll();
sum += cur;
pQ.add(cur);
}
return sum;
}
5.3.4 投资项目 – 堆
每个项目有花费,有收获,问怎么选择才能得到最高的收益
思路:按花费组成小根堆,将花费小于等于手头资金的项目解锁,按花费组成大根堆
public static class Node {
public int p;
public int c;
public Node(int p, int c) {
this.p = p;
this.c = c;
}
}
public static class MinCostComparator implements Comparator<Node> {
@Override
public int compare(Node o1, Node o2) {
return o1.c - o2.c;
}
}
public static class MaxProfitComparator implements Comparator<Node> {
@Override
public int compare(Node o1, Node o2) {
return o2.p - o1.p;
}
}
public static int findMaximizedCapital(int k, int W, int[] Profits, int[] Capital) {
PriorityQueue<Node> minCostQ = new PriorityQueue<>(new MinCostComparator());
PriorityQueue<Node> maxProfitQ = new PriorityQueue<>(new MaxProfitComparator());
for (int i = 0; i < nodes.length; i++) {
minCostQ.add(new Node(Profits[i], Capital[i]););
}
for (int i = 0; i < k; i++) {
while (!minCostQ.isEmpty() && minCostQ.peek().c <= W) {
maxProfitQ.add(minCostQ.poll());
}
if (maxProfitQ.isEmpty()) {
return W;
}
W += maxProfitQ.poll().p;
}
return W;
}
5.3.5 随时获得中位数
- 一个数据流中,可以随时获得中位数
思路:准备一个大根堆放较小的一半,一个小根堆放较大的一半
每当有一个新的数字进入,马上和大根堆顶比较
若cur <= 大根堆堆顶,放入大根堆,否则加入小根堆
比较大根堆和小根堆大小,若 |size(大根堆) - size(小根堆)| > 2,则较大的那个弹出一个去较小的那个
public static class MedianHolder {
private PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>(new MaxHeapComparator());// 大根堆
private PriorityQueue<Integer> minHeap = new PriorityQueue<Integer>(new MinHeapComparator());// 小根堆
private void modifyTwoHeapsSize() {
if (this.maxHeap.size() == this.minHeap.size() + 2) {
this.minHeap.add(this.maxHeap.poll());
}
if (this.minHeap.size() == this.maxHeap.size() + 2) {
this.maxHeap.add(this.minHeap.poll());
}
}
public void addNumber(int num) {
if (maxHeap.isEmpty() || num <= maxHeap.peek()) {
maxHeap.add(num);
} else {
minHeap.add(num);
}
modifyTwoHeapsSize();
}
public Integer getMedian() {
int maxHeapSize = this.maxHeap.size();
int minHeapSize = this.minHeap.size();
if (maxHeapSize + minHeapSize == 0) {
return null;
}
Integer maxHeapHead = this.maxHeap.peek();
Integer minHeapHead = this.minHeap.peek();
if (((maxHeapSize + minHeapSize) & 1) == 0) {
return (maxHeapHead + minHeapHead) / 2;
}
return maxHeapSize > minHeapSize ? maxHeapHead : minHeapHead;
}
}
public static class MaxHeapComparator implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
if (o2 > o1) {
return 1;
} else {
return -1;
}
}
}
public static class MinHeapComparator implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
if (o2 < o1) {
return 1;
} else {
return -1;
}
}
}
*6. 暴力递归
6.1 基本思路
暴力递归就是尝试,是动态规划的基础
- 把问题转化为规模缩小了的同类问题的子问题
- 有明确的不需要继续进行递归的条件**(base case)**
- 有当得到了子问题的结果之后的决策过程
- 不记录每一个子问题的解
6.2 题目
6.2.1 汉诺塔问题
大圆盘不能放在小圆盘上面,给定层数N,打印出层数
public static void hanoi(int n) {
if (n > 0) {
func(n, n, "left", "mid", "right");
}
}
public static void func(int rest, int down, String from, String help, String to) {
if (rest == 1) {
System.out.println("move " + down + " from " + from + " to " + to);
} else {
func(rest - 1, down - 1, from, to, help);
func(1, down, from, help, to);
func(rest - 1, down - 1, help, from, to);
}
}
6.2.2 字符串的全部子序列 – 分支问题 – 从左到右
打印一个字符串的全部子序列,包括空字符串
public static void printAllSubsquence(String str) {
char[] chs = str.toCharArray();
process(chs, 0);
}
public static void process(char[] chs, int i) {
if (i == chs.length) {
System.out.println(String.valueOf(chs));
return;
}
process(chs, i + 1);
char tmp = chs[i];
chs[i] = 0;
process(chs, i + 1);
chs[i] = tmp;
}
6.2.3 打印一个字符串的全部排列 – 分支问题 – 从左到右
public static ArrayList<String> Permutation(String str) {
ArrayList<String> res = new ArrayList<>();
if (str == null || str.length() == 0) {
return res;
}
char[] chs = str.toCharArray();
process(chs, 0, res);
res.sort(null);
return res;
}
public static void process(char[] chs, int i, ArrayList<String> res) {
if (i == chs.length) {
res.add(String.valueOf(chs));
}
boolean[] visit = new boolean[26];// 为了不重复
for (int j = i; j < chs.length; j++) {
if (!visit[chs[j] - 'a']) {
visit[chs[j] - 'a'] = true;// 分支限界
swap(chs, i, j);
process(chs, i + 1, res);
swap(chs, i, j);
}
}
}
6.2.4 拿牌游戏 – 分支问题
给定一个整型数组arr,代表数值不同的纸牌排成一条线。
玩家A和玩家B依次拿走每张纸牌,规定玩家A先拿,玩家B后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家A和玩家B都绝顶聪明。
请返回最后获胜者的分数。
public static int win1(int[] arr) {
if (arr == null || arr.length == 0) {
return 0;
}
return Math.max(f(arr, 0, arr.length - 1), s(arr, 0, arr.length - 1));
}
public static int f(int[] arr, int i, int j) { // 先手
if (i == j) {
return arr[i];
}
return Math.max(arr[i] + s(arr, i + 1, j), arr[j] + s(arr, i, j - 1));
}
public static int s(int[] arr, int i, int j) { // 后手
if (i == j) {
return 0;
}
return Math.min(f(arr, i + 1, j), f(arr, i, j - 1));
}
public static int win2(int[] arr) {
if (arr == null || arr.length == 0) {
return 0;
}
int[][] f = new int[arr.length][arr.length];
int[][] s = new int[arr.length][arr.length];
for (int j = 0; j < arr.length; j++) {
f[j][j] = arr[j];
for (int i = j - 1; i >= 0; i--) {
f[i][j] = Math.max(arr[i] + s[i + 1][j], arr[j] + s[i][j - 1]);
s[i][j] = Math.min(f[i + 1][j], f[i][j - 1]);
}
}
return Math.max(f[0][arr.length - 1], s[0][arr.length - 1]);
}
6.2.5 反转一个栈 – 训练递归能力
给你一个栈,请你逆序这个栈,不能申请额外的数据结构,只能使用递归函数。如何实现?
public static void reverse(Stack<Integer> stack) {
if (stack.isEmpty()) {
return;
}
int i = getAndRemoveLastElement(stack); // 弹出栈底元素
reverse(stack);
stack.push(i);
}
public static int getAndRemoveLastElement(Stack<Integer> stack) {
int result = stack.pop();
if (stack.isEmpty()) {
return result;
} else {
int last = getAndRemoveLastElement(stack);
stack.push(result);
return last;
}
}
6.2.6 数字转化字符串 – 分支问题 – 从左到右
规定1和A对应、2和B对应、3和C对应…
那么一个数字字符串比如"111",就可以转化为"AAA"、“KA"和"AK”。
给定一个只有数字字符组成的字符串str,返回有多少种转化结果。
public static int number(String str) {
if (str == null || str.length() == 0) {
return 0;
}
return process(str.toCharArray(), 0);
}
public static int process(char[] chs, int i) {
if (i == chs.length) {
return 1;
}
if (chs[i] == '0') {
return 0;
}
if (chs[i] == '1') {
int res = process(chs, i + 1);
if (i + 1 < chs.length) {
res += process(chs, i + 2);
}
return res;
}
if (chs[i] == '2') {
int res = process(chs, i + 1);
if (i + 1 < chs.length && (chs[i + 1] >= '0' && chs[i + 1] <= '6')) {
res += process(chs, i + 2);
}
return res;
}
return process(chs, i + 1);
}
锁住0~i-1,i位置如果是0字符,那一定不可以,如果不是0字符,那再锁一个,继续迭代
如果i和i+1组成的是小于等于26的数字,则合法
6.2.7 最大货物问题
给定两个长度都为N的数组weights和values,weights[i]和values[i]分别代表i号物品的重量和价值。
给定一个正数bag,表示一个载重bag的袋子,你装的物品不能超过这个重量。
返回你能装下最多的价值是多少?
public static int maxValue1(int[] weights, int[] values, int bag) {
return process1(weights, values, 0, 0, bag);
}
public static int process1(int[] weights, int[] values, int i, int alreadyweight, int bag) {
if (alreadyweight > bag) {
return 0;
}
if (i == weights.length) {
return 0;
}
return Math.max(process1(weights, values, i + 1, alreadyweight, bag),// 不要这货
values[i] + process1(weights, values, i + 1, alreadyweight + weights[i], bag));// 要这个货
}
public static int maxValue2(int[] c, int[] p, int bag) {
int[][] dp = new int[c.length + 1][bag + 1];
for (int i = c.length - 1; i >= 0; i--) {
for (int j = bag; j >= 0; j--) {
dp[i][j] = dp[i + 1][j];
if (j + c[i] <= bag) {
dp[i][j] = Math.max(dp[i][j], p[i] + dp[i + 1][j + c[i]]);
}
}
}
return dp[0][0];
}
6.2.8 N皇后问题
在N*N的棋盘上要摆N个皇后,要求任何两个皇后不同行、不同列,也不在同一条斜线上。
给定一个整数N,返回N皇后的摆法有多少种。
- 如:
- n=1,返回1。
- n=2或3,2皇后和3皇后问题无论怎么摆都不行,返回0。
- n=8,返回92。
方法一:
public static int num1(int n) {
if (n < 1) return 0;
int[] record = new int[n];
return process1(0, record, n);
}
// 递归方法
public static int process1(int i, int[] record, int n) {//来到了第i行
if (i == n) return 1;// basecase,最后一行有且只有一种可能
int res = 0;
for (int j = 0; j < n; j++) {// 对第i行的第j列挨个看是不是有效
if (isValid(record, i, j)) {
record[i] = j;
res += process1(i + 1, record, n);// 递归,这个皇后有效的情况下,看下面的情况
}
}
return res;
}
public static boolean isValid(int[] record, int i, int j) {// 判读是不是合法
for (int k = 0; k < i; k++) {// 前i-1行里的recood有没有j,再看是不是斜线上的
if (j == record[k] || Math.abs(record[k] - j) == Math.abs(i - k)) {
return false;
}
}
return true;
}
方法二:常数优化、位运算加速,加速了检查某个位置的合法性
public static int num2(int n) {
if (n < 1 || n > 32) return 0;// 32皇后以内 因为int是32位
int = n == 32 ? -1 : (1 << n) - 1;// 如8皇后问题,生成:0000 0000 1111 1111
return process2(upperLim, 0, 0, 0);
}
// colLim:列限制、leftDiaLim:下一行左对角线限制、rightDiaLim:下一行右对角线限制
public static int process2(int upperLim, int colLim, int leftDiaLim, int rightDiaLim) {
if (colLim == upperLim) {
return 1;
}
int pos = 0;
int mostRightOne = 0;
pos = upperLim & (~(colLim | leftDiaLim | rightDiaLim));
int res = 0;
while (pos != 0) {
mostRightOne = pos & (~pos + 1);// 最右边的一个
pos = pos - mostRightOne;// 去掉最右边这个
res += process2(upperLim,
colLim | mostRightOne,
(leftDiaLim | mostRightOne) << 1,
(rightDiaLim | mostRightOne) >>> 1);
}
return res;
}