链表与荷兰国旗问题
将单向链表按某值划分成左边小、中间相等、右边大的形式
复制代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | #include<stdio.h> #include "LinkList.cpp" /* partition一个链表有两种做法。 1 ,将链表中的所有结点放入一个数组中,那么就转换成了荷兰国旗问题,但这种做***使用O(N)的额外空间; 2 ,分出逻辑上的small,equal,big三个区域,遍历链表结点将其添加到对应的区域中,最后再将这三个区域连起来。 这里只示范第二种做法: */ void partitionList(LinkNode *head, int val){ if (head == NULL) return ; LinkNode *smH = NULL; //small area head node LinkNode *smT = NULL; //small area tail node LinkNode *midH = NULL; //equal area head node LinkNode *midT = NULL; //equal area tail node LinkNode *bigH = NULL; //big area head node LinkNode *bigT = NULL; //big area tail node LinkNode *cur = head->next; LinkNode *next = NULL; //next node need to be distributed to the three areas while (cur != NULL){ next = cur->next; cur->next = NULL; if (cur->data > val){ if (bigH == NULL){ bigH = bigT = cur; } else { bigT->next = cur; bigT = cur; } } else if (cur->data == val){ if (midH == NULL){ midH = midT = cur; } else { midT->next = cur; midT = cur; } } else { if (smH == NULL){ smH = smT = cur; } else { smT->next = cur; smT = cur; } } cur = next; } //reconnect small and equal if (smT != NULL){ smT->next = midH; midT = midT == NULL ? midT : smT; } //reconnect equal and big if (bigT != NULL){ midT->next = bigH; } head = smH != NULL ? smH : midH != NULL ? midH : bigH; return ; } int main(){ LinkNode* head; init(head); add( 5 ,head); add( 2 ,head); add( 7 ,head); add( 9 ,head); add( 1 ,head); add( 3 ,head); add( 5 ,head); printList(head); partitionList(head, 5 ); printList(head); } |
复制含有随机指针结点的链表
借助哈希表,额外空间O(N)
将链表的所有结点复制一份,以key,value为源结点,副本结点的方式存储到哈希表中,再建立副本结点之间的关系(next、rand指针域)
复制代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | import java.util.HashMap; import java.util.Map; public class CopyLinkListWithRandom { public static class Node { public Node( int data) { this .data = data; } public Node() { } int data; Node next; Node rand; } public static Node copyLinkListWithRandom(Node head) { if (head == null ) { return null ; } Node cur = head; Map<Node, Node> copyMap = new HashMap<>(); while (cur != null ) { copyMap.put(cur, new Node(cur.data)); cur = cur.next; } cur = head; while (cur != null ) { copyMap.get(cur).next = copyMap.get(cur.next); copyMap.get(cur).rand = copyMap.get(cur.rand); cur = cur.next; } return copyMap.get(head); } public static void printListWithRandom(Node head) { if (head != null ) { while (head.next != null ) { head = head.next; System.out.print( "node data:" + head.data); if (head.rand != null ) { System.out.println( ",rand data:" + head.rand.data); } else { System.out.println( ",rand is null" ); } } } } public static void main(String[] args) { Node head = new Node(); head.next = new Node( 1 ); head.next.next = new Node( 2 ); head.next.next.next = new Node( 3 ); head.next.next.next.next = new Node( 4 ); head.next.rand = head.next.next.next.next; head.next.next.rand = head.next.next.next; printListWithRandom(head); System.out.println( "==========" ); Node copy = copyLinkListWithRandom(head); printListWithRandom(copy); } } |
进阶操作:额外空间O(1)
将副本结点追加到对应源结点之后,建立副本结点之间的指针域,最后将副本结点从该链表中分离出来。
复制代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | //extra area O(1) public static Node copyLinkListWithRandom2(Node head){ if (head == null ) { return null ; } Node cur = head; //copy every node and append while (cur != null ) { Node copy = new Node(cur.data); copy.next = cur.next; cur.next = copy; cur = cur.next.next; } //set the rand pointer of every copy node Node copyHead = head.next; cur = head; Node curCopy = copyHead; while (curCopy != null ) { curCopy.rand = cur.rand == null ? null : cur.rand.next; cur = curCopy.next; curCopy = cur == null ? null : cur.next; } //split cur = head; Node next = null ; while (cur != null ) { curCopy = cur.next; next = cur.next.next; curCopy.next = next == null ? null : next.next; cur.next = next; cur = next; } return copyHead; } |
若两个可能有环的单链表相交,请返回相交的第一个结点
根据单链表的定义,每个结点有且只有一个next指针,那么如果单链表有环,它的结构将是如下所示:
相交会导致两个结点指向同一个后继结点,但不可能出现一个结点有两个后继结点的情况。
1、当相交的结点不在环上时,有如下两种情况:
2、当相交的结点在环上时,只有一种情况:
综上,两单链表若相交,要么都无环,要么都有环。
此题还需要注意的一点是如果链表有环,那么如何获取入环呢(因为不能通过next是否为空来判断是否是尾结点了)。这里就涉及到了一个规律:如果快指针fast和慢指针slow同时从头结点出发,fast走两步而slow走一步,当两者相遇时,将fast指针指向头结点,使两者都一次只走一步,两者会在入环结点相遇。
复制代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | public class FirstIntersectNode { public static class Node{ int data; Node next; public Node( int data) { this .data = data; } } public static Node getLoopNode(Node head) { if (head == null ) { return null ; } Node fast = head; Node slow = head; do { slow = slow.next; if (fast.next == null || fast.next.next == null ) { return null ; } else { fast = fast.next.next; } } while (fast != slow); //fast == slow fast = head; while (fast != slow) { slow = slow.next; fast = fast.next; } return slow; } public static Node getFirstIntersectNode(Node head1, Node head2) { if (head1 == null || head2 == null ) { return null ; } Node loop1 = getLoopNode(head1); //两链表的入环结点loop1和loop2 Node loop2 = getLoopNode(head2); //no loop if (loop1 == null && loop2 == null ) { return noLoop(head1, head2); } //both loop if (loop1 != null && loop2 != null ) { return bothLoop(head1, head2, loop1, loop2); } //don't intersect return null ; } private static Node bothLoop(Node head1, Node head2, Node loop1, Node loop2) { Node cur1 = head1; Node cur2 = head2; //入环结点相同,相交点不在环上 if (loop1 == loop2) { int n = 0 ; while (cur1.next != loop1) { n++; cur1 = cur1.next; } while (cur2.next != loop1) { n--; cur2 = cur2.next; } cur1 = n > 0 ? head1 : head2; //将cur1指向结点数较多的链表 cur2 = cur1 == head1 ? head2 : head1; //将cur2指向另一个链表 n = Math.abs(n); while (n != 0 ) { //将cur1先走两链表结点数差值个结点 cur1 = cur1.next; n--; } while (cur1 != cur2) { //cur1和cur2会在入环结点相遇 cur1 = cur1.next; cur2 = cur2.next; } return cur1; } //入环结点不同,相交点在环上 cur1 = loop1.next; while (cur1 != loop1) { if (cur1 == loop2) { //链表2的入环结点在链表1的环上,说明相交 return loop1; //返回loop1或loop2均可,因为整个环就是两链表的相交部分 } cur1 = cur1.next; } //在链表1的环上转了一圈也没有找到链表2的入环结点,说明不想交 return null ; } private static Node noLoop(Node head1, Node head2) { Node cur1 = head1; Node cur2 = head2; int n = 0 ; while (cur1.next != null ) { n++; cur1 = cur1.next; } while (cur2.next != null ) { n--; cur2 = cur2.next; } if (cur1 != cur2) { //两链表的尾结点不同,不可能相交 return null ; } cur1 = n > 0 ? head1 : head2; //将cur1指向结点数较多的链表 cur2 = cur1 == head1 ? head2 : head1; //将cur2指向另一个链表 n = Math.abs(n); while (n != 0 ) { //将cur1先走两链表结点数差值个结点 cur1 = cur1.next; n--; } while (cur1 != cur2) { //cur1和cur2会在入环结点相遇 cur1 = cur1.next; cur2 = cur2.next; } return cur1; } public static void printList(Node head) { for ( int i = 0 ; i < 50 ; i++) { System.out.print(head.data+ " " ); head = head.next; } System.out.println(); } } |
对应三种情况测试如下:
复制代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | public static void main(String[] args) { //==================== both loop ====================== //1->2->[3]->4->5->6->7->[3]... Node head1 = new Node( 1 ); head1.next = new Node( 2 ); head1.next.next = new Node( 3 ); head1.next.next.next = new Node( 4 ); head1.next.next.next.next = new Node( 5 ); head1.next.next.next.next.next = new Node( 6 ); head1.next.next.next.next.next.next = new Node( 7 ); head1.next.next.next.next.next.next.next = head1.next.next; //9->8->[6]->7->3->4->5->[6]... Node head2 = new Node( 9 ); head2.next = new Node( 8 ); head2.next.next = head1.next.next.next.next.next; head2.next.next.next = head1.next.next.next.next.next.next; head2.next.next.next.next = head1.next.next; head2.next.next.next.next.next = head1.next.next.next; head2.next.next.next.next.next.next = head1.next.next.next.next; head2.next.next.next.next.next.next.next = head1.next.next.next.next.next; printList(head1); printList(head2); System.out.println(getFirstIntersectNode(head1, head2).data); System.out.println( "==================" ); //1->[2]->3->4->5->6->7->8->4... Node head3 = new Node( 1 ); head3.next = new Node( 2 ); head3.next.next = new Node( 3 ); head3.next.next.next = new Node( 4 ); head3.next.next.next.next = new Node( 5 ); head3.next.next.next.next.next = new Node( 6 ); head3.next.next.next.next.next.next = new Node( 7 ); head3.next.next.next.next.next.next.next = new Node( 8 ); head3.next.next.next.next.next.next.next.next = head1.next.next.next; //9->0->[2]->3->4->5->6->7->8->4... Node head4 = new Node( 9 ); head4.next = new Node( 0 ); head4.next.next = head3.next; head4.next.next.next = head3.next.next; head4.next.next.next.next = head3.next.next.next; head4.next.next.next.next.next = head3.next.next.next.next; head4.next.next.next.next.next.next = head3.next.next.next.next.next; head4.next.next.next.next.next.next.next = head3.next.next.next.next.next.next; head4.next.next.next.next.next.next.next.next = head3.next.next.next.next.next.next.next; head4.next.next.next.next.next.next.next.next.next = head3.next.next.next; printList(head3); printList(head4); System.out.println(getFirstIntersectNode(head3,head4).data); System.out.println( "==================" ); //============= no loop ============== //1->[2]->3->4->5 Node head5 = new Node( 1 ); head5.next = new Node( 2 ); head5.next.next = new Node( 3 ); head5.next.next.next = new Node( 4 ); head5.next.next.next.next = new Node( 5 ); //6->[2]->3->4->5 Node head6 = new Node( 6 ); head6.next = head5.next; head6.next.next = head5.next.next; head6.next.next.next = head5.next.next.next; head6.next.next.next.next = head5.next.next.next.next; System.out.println(getFirstIntersectNode(head5,head6).data); } |
栈和队列
用数组结构实现大小固定的栈和队列
复制代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | // // Created by zaw on 2018/10/21. // #include <stdio.h> #include <malloc.h> #define MAX_SIZE 1000 struct ArrayStack{ int data[MAX_SIZE]; int top; }; void init(ArrayStack *&stack) { stack = (ArrayStack *) malloc(sizeof(ArrayStack)); stack->top = - 1 ; } bool isEmpty(ArrayStack* stack){ return stack->top == - 1 ?; } bool isFull(ArrayStack *stack){ return stack->top == MAX_SIZE - 1 ?; } void push( int i, ArrayStack *stack){ if (!isFull(stack)) { stack->data[++stack->top] = i; } } int pop(ArrayStack* stack){ if (!isEmpty(stack)) { return stack->data[stack->top--]; } } int getTopElement(ArrayStack *stack){ if (!isEmpty(stack)) { return stack->data[stack->top]; } } int main(){ ArrayStack* stack; init(stack); push( 1 , stack); push( 2 , stack); push( 3 , stack); printf( "%d " , pop(stack)); printf( "%d " , getTopElement(stack)); printf( "%d " , pop(stack)); printf( "%d " , pop(stack)); //3 2 2 1 return 0 ; } |
复制代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | // // Created by zaw on 2018/10/21. // #include <stdio.h> #include <malloc.h> #define MAX_SIZE 1000 //数组结构实现的环形队列 struct ArrayCircleQueue{ int data[MAX_SIZE]; int front,rear; }; void init(ArrayCircleQueue *&queue){ queue = (ArrayCircleQueue *) malloc(sizeof(ArrayCircleQueue)); queue->front = queue->rear = 0 ; } bool isEmpty(ArrayCircleQueue *queue){ return queue->front == queue->rear; } bool isFull(ArrayCircleQueue *queue){ return (queue->rear+ 1 )%MAX_SIZE==queue->front; } void enQueue( int i, ArrayCircleQueue *queue){ if (!isFull(queue)) { //move the rear and fill it queue->data[++queue->rear] = i; } } int deQueue(ArrayCircleQueue *queue){ if (!isEmpty(queue)) { return queue->data[++queue->front]; } } int main(){ ArrayCircleQueue* queue; init(queue); enQueue( 1 , queue); enQueue( 2 , queue); enQueue( 3 , queue); while (!isEmpty(queue)) { printf( "%d " , deQueue(queue)); } } |
取栈中最小元素
实现一个特殊的栈,在实现栈的基本功能的基础上,再实现返回栈中最小元素的操作getMin。要求如下:
- pop、push、getMin操作的时间复杂度都是O(1)。
- 设计的栈类型可以使用现成的栈结构。
思路:由于每次push之后都会可能导致栈中已有元素的最小值发生变化,因此需要一个容器与该栈联动(记录每次push产生的栈中最小值)。我们可以借助一个辅助栈,数据栈push第一个元素时,将其也push到辅助栈,此后每次向数据栈push元素的同时将其和辅助栈的栈顶元素比较,如果小,则将其也push到辅助栈,否则取辅助栈的栈顶元素push到辅助栈。(数据栈正常push、pop数据,而辅助栈push每次数据栈push后产生的栈中最小值;但数据栈pop时,辅助栈也只需简单的pop即可,保持同步)
复制代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | // // Created by zaw on 2018/10/21. // #include <stdio.h> #include <malloc.h> #include "ArrayStack.cpp" int min( int a, int b){ return a < b ? a : b; } struct GetMinStack{ ArrayStack* dataStack; ArrayStack* helpStack; }; void initGetMinStack(GetMinStack* &stack){ stack = (GetMinStack *) malloc(sizeof(GetMinStack)); init(stack->dataStack); init(stack->helpStack); } void push( int i, GetMinStack *stack) { if (!isFull(stack->dataStack)) { push(i, stack->dataStack); //ArrayStack.cpp if (!isEmpty(stack->helpStack)) { i = min(i, getTopElement(stack->helpStack)); } push(i, stack->helpStack); } } int pop(GetMinStack* stack){ if (!isEmpty(stack->dataStack)) { pop(stack->helpStack); return pop(stack->dataStack); } } int getMin(GetMinStack *stack){ if (!isEmpty(stack->dataStack)) { return getTopElement(stack->helpStack); } } int main(){ GetMinStack *stack; initGetMinStack(stack); push( 6 , stack); printf( "%d " , getMin(stack)); //6 push( 3 , stack); printf( "%d " , getMin(stack)); //3 push( 1 , stack); printf( "%d " , getMin(stack)); //1 pop(stack); printf( "%d " , getMin(stack)); //3 return 0 ; } |
仅用队列结构实现栈结构
思路:只要将关注点放在 后进先出 这个特性就不难实现了。使用一个数据队列和辅助队列,当放入数据时使用队列的操作正常向数据队列中放,但出队元素时,需将数据队列的前n-1个数入队辅助队列,而将数据队列的队尾元素弹出来,最后数据队列和辅助队列交换角色。
复制代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | // // Created by zaw on 2018/10/21. // #include <stdio.h> #include <malloc.h> #include "../queue/ArrayCircleQueue.cpp" struct DoubleQueueStack{ ArrayCircleQueue* dataQ; ArrayCircleQueue* helpQ; }; void init(DoubleQueueStack* &stack){ stack = (DoubleQueueStack *) malloc(sizeof(DoubleQueueStack)); init(stack->dataQ); init(stack->helpQ); } void swap(ArrayCircleQueue *&dataQ, ArrayCircleQueue *&helpQ){ ArrayCircleQueue* temp = dataQ; dataQ = helpQ; helpQ = temp; } void push( int i,DoubleQueueStack* stack){ if (!isFull(stack->dataQ)) { return enQueue(i, stack->dataQ); } } int pop(DoubleQueueStack* stack){ if (!isEmpty(stack->dataQ)) { int i = deQueue(stack->dataQ); while (!isEmpty(stack->dataQ)) { enQueue(i, stack->helpQ); i = deQueue(stack->dataQ); } swap(stack->dataQ, stack->helpQ); return i; } } bool isEmpty(DoubleQueueStack* stack){ return isEmpty(stack->dataQ); } int getTopElement(DoubleQueueStack* stack){ if (!isEmpty(stack->dataQ)) { int i = deQueue(stack->dataQ); while (!isEmpty(stack->dataQ)) { enQueue(i, stack->helpQ); i = deQueue(stack->dataQ); } enQueue(i, stack->helpQ); swap(stack->dataQ, stack->helpQ); return i; } } int main(){ DoubleQueueStack *stack; init(stack); push( 1 , stack); push( 2 , stack); push( 3 , stack); while (!isEmpty(stack)) { printf( "%d " , pop(stack)); } push( 4 , stack); printf( "%d " , getTopElement(stack)); return 0 ; } |
仅用栈结构实现队列结构
思路:使用两个栈,一个栈PutStack用来放数据,一个栈GetStack用来取数据。取数据时,如果PulllStack为空则需要将PutStack中的所有元素一次性依次pop并放入GetStack。
特别要注意的是这个 倒数据的时机:
- 只有当GetStack为空时才能往里倒
- 倒数据时必须一次性将PutStack中的数据倒完
复制代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | // // Created by zaw on 2018/10/21. // #include <stdio.h> #include <malloc.h> #include "../stack/ArrayStack.cpp" struct DoubleStackQueue{ ArrayStack* putStack; ArrayStack* getStack; }; void init(DoubleStackQueue *&queue){ queue = (DoubleStackQueue *) malloc(sizeof(DoubleStackQueue)); init(queue->putStack); init(queue->getStack); } bool isEmpty(DoubleStackQueue *queue){ return isEmpty(queue->getStack) && isEmpty(queue->putStack); } void pour(ArrayStack *stack1, ArrayStack *stack2){ while (!isEmpty(stack1)) { push(pop(stack1), stack2); } } void enQueue( int i, DoubleStackQueue *queue){ if (!isFull(queue->putStack)) { push(i, queue->putStack); } else { if (isEmpty(queue->getStack)) { pour(queue->putStack, queue->getStack); push(i, queue->putStack); } } } int deQueue(DoubleStackQueue* queue){ if (!isEmpty(queue->getStack)) { return pop(queue->getStack); } else { if (!isEmpty(queue->putStack)) { pour(queue->putStack, queue->getStack); return pop(queue->getStack); } } } int main(){ DoubleStackQueue *queue; init(queue); enQueue( 1 , queue); printf( "%d\n" , deQueue(queue)); enQueue( 2 , queue); enQueue( 3 , queue); while (!isEmpty(queue)) { printf( "%d " , deQueue(queue)); } return 0 ; } |
实现二叉树的先序、中序、后续遍历,包括递归方式和非递归方式
递归方式
复制代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | public static class Node{ int data; Node left; Node right; public Node( int data) { this .data = data; } } public static void preOrderRecursive(Node root) { if (root != null ) { System.out.print(root.data+ " " ); preOrderRecursive(root.left); preOrderRecursive(root.right); } } public static void medOrderRecursive(Node root) { if (root != null ) { medOrderRecursive(root.left); System.out.print(root.data+ " " ); medOrderRecursive(root.right); } } public static void postOrderRecursive(Node root) { if (root != null ) { postOrderRecursive(root.left); postOrderRecursive(root.right); System.out.print(root.data+ " " ); } } public static void main(String[] args) { Node root = new Node( 1 ); root.left = new Node( 2 ); root.right = new Node( 3 ); root.left.left = new Node( 4 ); root.left.right = new Node( 5 ); root.right.left = new Node( 6 ); root.right.right = new Node( 7 ); preOrderRecursive(root); //1 2 4 5 3 6 7 System.out.println(); medOrderRecursive(root); //4 2 5 1 6 3 7 System.out.println(); postOrderRecursive(root); //4 5 2 6 7 3 1 System.out.println(); } |
以先根遍历二叉树为例,可以发现递归方式首先尝试打印当前结点的值,随后尝试打印左子树,打印完左子树后尝试打印右子树,递归过程的base case是当某个结点为空时停止子过程的展开。这种递归尝试是由二叉树本身的结构所决定的,因为二叉树上的任意结点都可看做一棵二叉树的根结点(即使是叶子结点,也可以看做是一棵左右子树为空的二叉树根结点)。
观察先序、中序、后序三个递归方法你会发现,不同点在于打印当前结点的值这一操作的时机。你会发现每个结点会被访问三次:进入方法时算一次、递归处理左子树完成之后返回时算一次、递归处理右子树完成之后返回时算一次。因此在preOrderRecursive中将打印语句放到方法开始时就产生了先序遍历;在midOrderRecursive中,将打印语句放到递归chu处理左子树完成之后就产生了中序遍历。
非递归方式
先序遍历
拿到一棵树的根结点后,首先打印该结点的值,然后将其非空右孩子、非空左孩子依次压栈。栈非空循环:从栈顶弹出结点(一棵子树的根节点)并打印其值,再将其非空右孩子、非空左孩子依次压栈。
复制代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public static void preOrderUnRecur(Node root) { if (root == null ) { return ; } Stack<Node> stack = new Stack<>(); stack.push(root); Node cur; while (!stack.empty()) { cur = stack.pop(); System.out.print(cur.data+ " " ); if (cur.right != null ) { stack.push(cur.right); } if (cur.left != null ) { stack.push(cur.left); } } System.out.println(); } |
你会发现压栈的顺序和打印的顺序是相反的,压栈是先根结点,然后有右孩子就压右孩子、有左孩子就压左孩子,这是利用栈的后进先出。每次获取到一棵子树的根节点之后就可以获取其左右孩子,因此无需保留其信息,直接弹出并打印,然后保留其左右孩子到栈中即可。
中序遍历
对于一棵树,将该树的左边界全部压栈,root的走向是只要左孩子不为空就走向左孩子。当左孩子为空时弹出栈顶结点(此时该结点是一棵左子树为空的树的根结点,根据中序遍历可以直接打印该结点,然后中序遍历该结点的右子树)打印,如果该结点的右孩子非空(说明有右子树),那么将其右孩子压栈,这个右孩子又可能是一棵子树的根节点,因此将这棵子树的左边界压栈,这时回到了开头,以此类推。
复制代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public static void medOrderUnRecur(Node root) { if (root == null ) { return ; } Stack<Node> stack = new Stack<>(); while (!stack.empty() || root != null ) { if (root != null ) { stack.push(root); root = root.left; } else { root = stack.pop(); System.out.print(root.data+ " " ); root = root.right; } } System.out.println(); } |
后序遍历
思路一:准备两个栈,一个栈用来保存遍历时的结点信息,另一个栈用来排列后根顺序(根节点先进栈,右孩子再进,左孩子最后进)。
复制代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public static void postOrderUnRecur1(Node root) { if (root == null ) { return ; } Stack<Node> stack1 = new Stack<>(); Stack<Node> stack2 = new Stack<>(); stack1.push(root); while (!stack1.empty()) { root = stack1.pop(); if (root.left != null ) { stack1.push(root.left); } if (root.right != null ) { stack1.push(root.right); } stack2.push(root); } while (!stack2.empty()) { System.out.print(stack2.pop().data + " " ); } System.out.println(); } |
思路二:只用一个栈。借助两个变量h和c,h代表最近一次打印过的结点,c代表栈顶结点。首先将根结点压栈,此后栈非空循环,令c等于栈顶元素(c=stack.peek())执行以下三个分支:
- c的左右孩子是否与h相等,如果都不相等,说明c的左右孩子都不是最近打印过的结点,由于左右孩子是左右子树的根节点,根据后根遍历的特点,左右子树肯定都没打印过,那么将左孩子压栈(打印左子树)。
- 分支1没有执行说明c的左孩子要么不存在;要么左子树刚打印过了;要么右子树刚打印过了。这时如果是前两种情况中的一种,那就轮到打印右子树了,因此如果c的右孩子非空就压栈。
- 如果前两个分支都没执行,说明c的左右子树都打印完了,因此弹出并打印c结点,更新一下h。
复制代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public static void postOrderUnRecur2(Node root) { if (root == null ) { return ; } Node h = null ; //最近一次打印的结点 Node c = null ; //代表栈顶结点 Stack<Node> stack = new Stack<>(); stack.push(root); while (!stack.empty()) { c = stack.peek(); if (c.left != null && c.left != h && c.right != h) { stack.push(c.left); } else if (c.right != null && c.right != h) { stack.push(c.right); } else { System.out.print(stack.pop().data + " " ); h = c; } } System.out.println(); } |
在二叉树中找一个结点的后继结点,结点除lleft,right指针外还包含一个parent指针
这里的后继结点不同于链表的后继结点。在二叉树中,前驱结点和后继结点是按照二叉树中两个结点被中序遍历的先后顺序来划分的。比如某二叉树的中序遍历是2 1 3,那么1的后继结点是3,前驱结点是2
你当然可以将二叉树中序遍历一下,在遍历到该结点的时候标记一下,那么下一个要打印的结点就是该结点的后继结点。
我们可以推测一下,当我们来到二叉树中的某个结点时,如果它的右子树非空,那么它的后继结点一定是它的右子树中最靠左的那个结点;如果它的右孩子为空,那么它的后继结点一定是它的祖先结点中,把它当做左子孙(它存在于祖先结点的左子树中)的那一个,否则它没有后继结点。
这里如果它的右孩子为空的情况比较难分析,我们可以借助一个指针parent,当前来到的结点node和其父结点parent的parent.left比较,如果相同则直接返回parent,否则node来到parent的位置,parent则继续向上追溯,直到parent到达根节点为止若node还是不等于parent的左孩子,则返回null表明给出的结点没有后继结点。
复制代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | public class FindSuccessorNode { public static class Node{ int data; Node left; Node right; Node parent; public Node( int data) { this .data = data; } } public static Node findSuccessorNode(Node node){ if (node == null ) { return null ; } if (node.right != null ) { node = node.right; while (node.left != null ) { node = node.left; } return node; } else { Node parent = node.parent; while (parent != null && parent.left != node) { node = parent; parent = parent.parent; } return parent == null ? null : parent; } } public static void main(String[] args) { Node root = new Node( 1 ); root.left = new Node( 2 ); root.left.parent = root; root.left.left = new Node( 4 ); root.left.left.parent = root.left; root.left.right = new Node( 5 ); root.left.right.parent = root.left; root.right = new Node( 3 ); root.right.parent = root; root.right.right = new Node( 6 ); root.right.right.parent = root.right; if (findSuccessorNode(root.left.right) != null ) { System.out.println( "node5's successor node is:" +findSuccessorNode(root.left.right).data); } else { System.out.println( "node5's successor node doesn't exist" ); } if (findSuccessorNode(root.right.right) != null ) { System.out.println( "node6's successor node is:" +findSuccessorNode(root.right.right).data); } else { System.out.println( "node6's successor node doesn't exist" ); } } } |
介绍二叉树的序列化和反序列化
序列化
二叉树的序列化要注意的两个点如下:
- 每序列化一个结点数值之后都应该加上一个结束符表示一个结点序列化的终止,如!。
- 不能忽视空结点的存在,可以使用一个占位符如#表示空结点的序列化。
复制代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | /** * 先根遍历的方式进行序列化 * @param node 序列化来到了哪个结点 * <a href="/profile/547241" data-card-uid="547241" class="" target="_blank" from-niu="default" data-card-index="6">@return */ public static String serializeByPre(Node node) { if (node == null ) { return "#!" ; } //收集以当前结点为根节点的树的序列化信息 String res = node.data + "!" ; //假设能够获取左子树的序列化结果 res += serializeByPre(node.left); //假设能够获取右子树的序列化结果 res += serializeByPre(node.right); //返回以当前结点为根节点的树的序列化结果 return res; } public static void main(String[] args) { Node root = new Node( 1 ); root.left = new Node( 2 ); root.left.left = new Node( 4 ); root.left.right = new Node( 5 ); root.right = new Node( 3 ); root.right.right = new Node( 6 ); System.out.println(serializeByPre(root)); }</a> |
重建
怎么序列化的,就怎么反序列化
复制代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | public static Node reconstrut(String serializeStr) { if (serializeStr != null ) { String[] datas = serializeStr.split( "!" ); if (datas.length > 0 ) { //借助队列保存结点数值 Queue<String> queue = new LinkedList<>(); for (String data : datas) { queue.offer(data); } return recon(queue); } } return null ; } private static Node recon(Queue<String> queue) { //依次出队元素重建结点 String data = queue.poll(); //重建空结点,也是base case,当要重建的某棵子树为空时直接返回 if (data.equals( "#" )) { return null ; } //重建头结点 Node root = new Node(Integer.parseInt(data)); //重建左右子树 root.left = recon(queue); root.right = recon(queue); return root; } public static void main(String[] args) { Node root = new Node( 1 ); root.left = new Node( 2 ); root.left.left = new Node( 4 ); root.left.right = new Node( 5 ); root.right = new Node( 3 ); root.right.right = new Node( 6 ); String str = serializeByPre(root); Node root2 = reconstrut(str); System.out.println(serializeByPre(root2)); } |
平衡二叉树的定义:当二叉树的任意一棵子树的左子树的高度和右子树的高度相差不超过1时,该二叉树为平衡二叉树。
根据定义可知,要确认一个二叉树是否是平衡二叉树势必要遍历所有结点。而遍历到每个结点时,要想知道以该结点为根结点的子树是否是平衡二叉树,我们要收集两个信息:
- 该结点的左子树、右子树是否是平衡二叉树
- 左右子树的高度分别是多少,相差是否超过1
那么我们来到某个结点时(子过程),我们需要向上层(父过程)返回的信息就是该结点为根结点的树是否是平衡二叉树以及该结点的高度,这样的话,父过程就能继续向上层返回应该收集的信息。
复制代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | package top.zhenganwen.algorithmdemo.recursive; /** * 判断是否为平衡二叉树 */ public class IsBalanceBTree { public static class Node{ int data; Node left; Node right; public Node( int data) { this .data = data; } } /** * 遍历时,来到某个结点需要收集的信息 * 1、以该结点为根节点的树是否是平衡二叉树 * 2、该结点的高度 */ public static class ReturnData { public boolean isBalanced; public int height; public ReturnData( boolean isBalanced, int height) { this .isBalanced = isBalanced; this .height = height; } } public static ReturnData isBalancedBinaryTree(Node node){ if (node == null ) { return new ReturnData( true , 0 ); } ReturnData leftData = isBalancedBinaryTree(node.left); if (leftData.isBalanced == false ) { //只要有一棵子树不是平衡二叉树,则会一路返回false,该树的高度自然不必收集了 return new ReturnData( false , 0 ); } ReturnData rightDta = isBalancedBinaryTree(node.right); if (rightDta.isBalanced == false ) { return new ReturnData( false , 0 ); } //返回该层收集的结果 if (Math.abs(leftData.height - rightDta.height) > 1 ) { return new ReturnData( false , 0 ); } //若是平衡二叉树,树高等于左右子树较高的那个加1 return new ReturnData( true , Math.max(leftData.height, rightDta.height) + 1 ); } public static void main(String[] args) { Node root = new Node( 1 ); root.left = new Node( 2 ); root.left.left = new Node( 4 ); root.right = new Node( 3 ); root.right.right = new Node( 5 ); root.right.right.right = new Node( 6 ); System.out.println(isBalancedBinaryTree(root).isBalanced); //false } } |
递归很好用,该题中的递归用法也是一种经典用法,可以高度套路:
- 分析问题的解决需要哪些步骤(这里是遍历每个结点,确认每个结点为根节点的子树是否为平衡二叉树)
- 确定递归:父问题是否和子问题相同
- 子过程要收集哪些信息
- 本次递归如何利用子过程返回的信息得到本过程要返回的信息
- base case
判断一棵树是否是搜索二叉树
搜索二叉树的定义:对于二叉树的任意一棵子树,其左子树上的所有结点的值小于该子树的根节点的值,而其右子树上的所有结点的值大于该子树的根结点的值,并且整棵树上任意两个结点的值不同。
根据定义,搜索二叉树的中序遍历打印将是一个升序序列。因此我们可以利用二叉树的中序遍历的非递归方式,比较中序遍历时相邻两个结点的大小,只要有一个结点的值小于其后继结点的那就不是搜索二叉树。
复制代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | import java.util.Stack; /** * 判断是否是搜索二叉树 */ public class IsBST { public static class Node { int data; Node left; Node right; public Node( int data) { this .data = data; } } public static boolean isBST(Node root) { if (root == null ) { return true ; } int preData = Integer.MIN_VALUE; Stack<Node> stack = new Stack<>(); while (root != null || !stack.empty()) { if (root != null ) { stack.push(root); root = root.left; } else { Node node = stack.pop(); if (node.data < preData) { return false ; } else { preData = node.data; } root = node.right; } } return true ; } public static void main(String[] args) { Node root = new Node( 6 ); root.left = new Node( 3 ); root.left.left = new Node( 1 ); root.left.right = new Node( 4 ); root.right = new Node( 8 ); root.right.left = new Node( 9 ); root.right.right = new Node( 10 ); System.out.println(isBST(root)); //false } } |
判断一棵树是否是完全二叉树
根据完全二叉树的定义,如果二叉树上某个结点有右孩子无左孩子则一定不是完全二叉树;否则如果二叉树上某个结点有左孩子而没有右孩子,那么该结点所在的那一层上,该结点右侧的所有结点应该是叶子结点,否则不是完全二叉树。
复制代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | import java.util.LinkedList; import java.util.Queue; /** * 判断是否为完全二叉树 */ public class IsCompleteBTree { public static class Node { int data; Node left; Node right; public Node( int data) { this .data = data; } } public static boolean isCompleteBTree(Node root) { if (root == null ) { return true ; } Queue<Node> queue = new LinkedList<>(); queue.offer(root); boolean leaf = false ; while (!queue.isEmpty()) { Node node = queue.poll(); //左空右不空 if (node.left == null && node.right != null ) { return false ; } //如果开启了叶子结点阶段,结点不能有左右孩子 if (leaf && (node.left != null || node.right != null )) { return false ; } //将下一层要遍历的加入到队列中 if (node.left != null ) { queue.offer(node.left); } if (node.right != null ) { queue.offer(node.right); } else { //左右均为空,或左不空右空。该结点同层的右侧结点均为叶子结点,开启叶子结点阶段 leaf = true ; } } return true ; } public static void main(String[] args) { Node root = new Node( 1 ); root.left = new Node( 2 ); root.right = new Node( 3 ); root.left.right = new Node( 4 ); System.out.println(isCompleteBTree(root)); //false } } |
已知一棵完全二叉树,求其结点个数,要求时间复杂度0(N)
如果我们遍历二叉树的每个结点来计算结点个数,那么时间复杂度将是O(N^2),我们可以利用满二叉树的结点个数为2^h-1(h为树的层数)来加速这个过程。
首先完全二叉树,如果其左子树的最左结点在树的最后一层,那么其右子树肯定是满二叉树,且高度为h-1;否则其左子树肯定是满二叉树,且高度为h-2。也就是说,对于一个完全二叉树结点个数的求解,我们可以分解求解过程:1个根结点+ 一棵满二叉树(高度为h-1或者h-2)+ 一棵完全二叉树(高度为h-1)。前两者的结点数是可求的(1+2^level -1=2^level),后者就又成了求一棵完全二叉树结点数的问题了,可以使用递归。
复制代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | /** * 求一棵完全二叉树的节点个数 */ public class CBTNodesNum { public static class Node { int data; Node left; Node right; public Node( int data) { super (); this .data = data; } } // 获取完全二叉树的高度 public static int getLevelOfCBT(Node root) { if (root == null ) return 0 ; int level = 0 ; while (root != null ) { level++; root = root.left; } return level; } public static int getNodesNum(Node node) { //base case if (node == null ) return 0 ; int level = getLevelOfCBT(node); if (getLevelOfCBT(node.right) == level - 1 ) { // 左子树满,且高度为 level-1;收集左子树节点数2^(level-1)-1和头节点,对右子树重复此过程 int leftNodesAndRoot = 1 << (level - 1 ); return getNodesNum(node.right) + leftNodesAndRoot; } else { // 右子树满,且高度为 level-2;收集右子树节点数2^(level-2)-1和头节点1,对左子树重复此过程 int rightNodesAndRoot = 1 << (level - 2 ); return getNodesNum(node.left) + rightNodesAndRoot; } } public static void main(String[] args) { Node root = new Node( 1 ); root.left = new Node( 2 ); root.right = new Node( 3 ); root.left.left = new Node( 4 ); root.left.right = new Node( 5 ); root.right.left = new Node( 6 ); root.right.right = new Node( 7 ); System.out.println(getNodesNum(root)); } } |
并查集
并查集是一种树型的数据结构,用于处理一些不交集(Disjoint Sets)的合并及查询问题。有一个联合-查找算法(union-find algorithm)定义了两个用于此数据结构的操作:
- Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
- Union:将两个子集合并成同一个集合。
并查集结构的实现
首先并查集本身是一个结构,我们在构造它的时候需要将所有要操作的数据扔进去,初始时每个数据自成一个结点,且每个结点都有一个父指针(初始时指向自己)。
初始时并查集中的每个结点都算是一个子集,我们可以对任意两个元素进行合并操作。值得注意的是,union(nodeA,nodeB)并不是将结点nodeA和nodeB合并成一个集合,而是将nodeA所在的集合和nodeB所在的集合合并成一个新的子集:
那么合并两个集合的逻辑是什么呢?首先要介绍一下代表结点这个概念:找一结点所在集合的代表结点就是找这个集合中父指针指向自己的结点(并查集初始化时,每个结点都是各自集合的代表结点)。那么合并两个集合就是将结点个数较少的那个集合的代表结点的父指针指向另一个集合的代表结点:
还有一个find操作:查找两个结点是否所属同一个集合。我们只需判断两个结点所在集合的代表结点是否是同一个就可以了:
代码示例:
复制代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | import java.util.*; public class UnionFindSet{ public static class Node{ //whatever you like to store int , char , String ..etc } private Map<Node,Node> fatherMap; private Map<Node,Integer> nodesNumMap; //give me the all nodes need to save into the UnionFindSet public UnionFindSet(List<Node> nodes){ fatherMap = new HashMap(); nodesNumMap = new HashMap(); for (Node node : nodes){ fatherMap.put(node,node); nodesNumMap.put(node, 1 ); } } public void union(Node a,Node b){ if (a == null || b == null ){ return ; } Node rootOfA = getRoot(a); Node rootOfB = getRoot(b); if (rootOfA != rootOfB){ int numOfA = nodesNumMap.get(rootOfA); int numOfB = nodesNumMap.get(rootOfB); if (numOfA >= numOfB){ fatherMap.put(rootOfB , rootOfA); nodesNumMap.put(rootOfA, numOfA + numOfB); } else { fatherMap.put(rootOfA , rootOfB); nodesNumMap.put(rootOfB, numOfA + numOfB); } } } public boolean find(Node a,Node b){ if (a == null || b == null ){ return false ; } Node rootOfA = getRoot(a); Node rootOfB = getRoot(b); return rootOfA == rootOfB ? true : false ; } public Node getRoot(Node node){ if (node == null ){ return null ; } Node father = fatherMap.get(node); if (father != node){ father = fatherMap.get(father); } fatherMap.put(node, father); return father; } public static void main(String[] args){ Node a = new Node(); Node b = new Node(); Node c = new Node(); Node d = new Node(); Node e = new Node(); Node f = new Node(); Node[] nodes = {a,b,c,d,e,f}; UnionFindSet set = new UnionFindSet(Arrays.asList(nodes)); set.union(a, b); set.union(c, d); set.union(b, e); set.union(a, c); System.out.println(set.find(d,e)); } } |
你会发现union和find的过程中都会有找一个结点所在集合的代表结点这个过程,所以我把它单独抽出来成一个getRoot,而且利用递归做了一个优化:找一个结点所在集合的代表结点时,会不停地向上找父指针指向自己的结点,最后在递归回退时将沿途路过的结点的父指针改为直接指向代表结点:
诚然,这样做是为了提高下一次查找的效率。
并查集的应用
并查集结构本身其实很简单,但是其应用却很难。这里以岛问题做引子,当矩阵相当大的时候,用单核CPU去跑这个遍历和感染效率是很低的,可能会使用并行计算框架来完成岛数量的统计。也就是说矩阵可能被分割成几个部分,逐个统计,最后在汇总。那么问题来了:
上面这个矩阵的岛数量是1;但如果从中间竖着切开,那么左边的岛数量是1,右边的岛数量是2,总数是3。如何处理切割后,相邻子矩阵之间的边界处的1相邻导致的重复统计呢?其实利用并查集的特性就很容易解决这个问题:
首先将切割边界处的数据封装成结点加入到并查集中并合并同一个岛上的结点,在分析边界时,查边界两边的1是否在同一个集合,如果不在那就union这两个结点,并将总的岛数量减1;否则就跳过此行继续分析下一行边界上的两个点。
标签:Node,结点,Java,左神,next,nowcoder,new,root,stack
来源: https://blog.csdn.net/qq_40126686/article/details/115417274