左神算法入门笔记

左神算法入门笔记

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 一个单链表是否有环

a
b
c
d
e
#
A
B
C
D
E

上面为无环,下面为有环,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,可能有环,判断它们相交与否,若相交,返回第一个相交结点

先判断是不是有环:

  1. 都无环,有两种情况
a
b
c
d
null
A
B
C
D
null
E
F
G
H
null
I
J
K

​ 找这两个链表的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;// 返回相遇的点
}
  1. 一个有环另一个无环,必然无交点

  2. 两个都有环,三种情况

    这个时候会返回两个入环节点:loop1和loop2

    ​ 若loop1 == loop2,则如下图,则相交结点在入环节点之前,用1.里的方法可以得到b

a
b
c
d
e
f
h
i
j

​ 让loop1往后走,转一圈到自己,若没有遇到loop2,如下图,两链表必然不相交

a
b
c
d
e
f
h
i
j
k
l
m

​ 若遇到了loop2,如下图所示,则loop1或loop2都是相交结点,任返回一个

a
b
c
d
e
f
h
i
j
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;
}

一颗二叉树:

1
2
3
4
5
6
7

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 宽度优先遍历

1
2
3
4
5
6
7
8
n

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 后继节点

A
B
C
D
E
F
G
  • B就是D的后继节点,B的后继节点是E
D
B
E
A
F
c
G
  • 题目:如果节点中有指向父节点的指针,理论上只需要走层数距离就可以找到后继节点

分析:

  • 如果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 图的表达方式

  • 两个要素:点集、边集
A
C
D
B

4.1.1 邻接表法:

A: C, D

B: C

C: A, B, D

D: A, C

4.1.2 邻接矩阵

ABCD
A0infww
Binf0winf
Cww0w
Dwinfw0

掌握一种结构,用这一种结构把所有算法实现一次,后续只需要写其他结构的接口

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
B
C
D

先找入度为零的点(A),它一定为起点,接下来去掉和A有关的边

B
C
D

会有第二个入度为零的点(B),它就排在A后面,接下来去掉和B有关的所有的边

C
D

以此类推,每次找入度为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

ABCDE
A0infinfinfinf

初始化一个表,从左到右依次取点,这个点之前锁死,以这个点为起点连成的路径若能使表格内数据更小,更新数据

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 解题技巧

贪心算法的在笔试时的解题套路:

  1. 实现一个不依靠贪心策略的解法X,可以用最暴力的尝试
  2. 脑补出贪心策略A、贪心策略B、贪心策略C…
  3. 用解法X和对数器,去验证每一个贪心策略,用实验的方式得知哪个贪心策略正确
  4. 不要去纠结贪心策略的证明

常用技巧:

  1. 建立比较器排序
  2. 建立比较器来构造堆

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 基本思路

暴力递归就是尝试,是动态规划的基础

  1. 把问题转化为规模缩小了的同类问题的子问题
  2. 有明确的不需要继续进行递归的条件**(base case)**
  3. 有当得到了子问题的结果之后的决策过程
  4. 不记录每一个子问题的解

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;
}
  • 7
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值