連結串列、棧、佇列、遞迴、雜湊
連結串列
單向連結串列
單向連結串列的節點結構(可以實現成泛型) :
public class Node {
public int value;
public Node next;
public Node(int data) {
value = data;
}
}
雙向連結串列
雙向連結串列的節點結構(可以實現成功泛型):
public static class DoubleNode {
public int value;
public DoubleNode last;
public DoubleNode next;
public DoubleNode(int data) {
value = data;
}
}
單雙連結串列簡單練習
- 單連結串列和雙連結串列如何反轉
1 -> 2 -> 3 轉換為 3 -> 2 -> 1
package class02;
import java.util.ArrayList;
public class Code01_ReverseList {
public static class Node {
public int value;
public Node next;
public Node(int data) {
value = data;
}
}
public static class DoubleNode {
public int value;
public DoubleNode last;
public DoubleNode next;
public DoubleNode(int data) {
value = data;
}
}
// 翻轉單向連結串列,傳入頭結點
public static Node reverseLinkedList(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 reverseDoubleList(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 Node testReverseLinkedList(Node head) {
if (head == null) {
return null;
}
ArrayList<Node> list = new ArrayList<>();
while (head != null) {
list.add(head);
head = head.next;
}
list.get(0).next = null;
int N = list.size();
for (int i = 1; i < N; i++) {
list.get(i).next = list.get(i - 1);
}
return list.get(N - 1);
}
public static DoubleNode testReverseDoubleList(DoubleNode head) {
if (head == null) {
return null;
}
ArrayList<DoubleNode> list = new ArrayList<>();
while (head != null) {
list.add(head);
head = head.next;
}
list.get(0).next = null;
DoubleNode pre = list.get(0);
int N = list.size();
for (int i = 1; i < N; i++) {
DoubleNode cur = list.get(i);
cur.last = null;
cur.next = pre;
pre.last = cur;
pre = cur;
}
return list.get(N - 1);
}
public static Node generateRandomLinkedList(int len, int value) {
int size = (int) (Math.random() * (len + 1));
if (size == 0) {
return null;
}
size--;
Node head = new Node((int) (Math.random() * (value + 1)));
Node pre = head;
while (size != 0) {
Node cur = new Node((int) (Math.random() * (value + 1)));
pre.next = cur;
pre = cur;
size--;
}
return head;
}
public static DoubleNode generateRandomDoubleList(int len, int value) {
int size = (int) (Math.random() * (len + 1));
if (size == 0) {
return null;
}
size--;
DoubleNode head = new DoubleNode((int) (Math.random() * (value + 1)));
DoubleNode pre = head;
while (size != 0) {
DoubleNode cur = new DoubleNode((int) (Math.random() * (value + 1)));
pre.next = cur;
cur.last = pre;
pre = cur;
size--;
}
return head;
}
// 要求無環,有環別用這個函式
public static boolean checkLinkedListEqual(Node head1, Node head2) {
while (head1 != null && head2 != null) {
if (head1.value != head2.value) {
return false;
}
head1 = head1.next;
head2 = head2.next;
}
return head1 == null && head2 == null;
}
// 要求無環,有環別用這個函式
public static boolean checkDoubleListEqual(DoubleNode head1, DoubleNode head2) {
boolean null1 = head1 == null;
boolean null2 = head2 == null;
if (null1 && null2) {
return true;
}
if (null1 ^ null2) {
return false;
}
if (head1.last != null || head2.last != null) {
return false;
}
DoubleNode end1 = null;
DoubleNode end2 = null;
while (head1 != null && head2 != null) {
if (head1.value != head2.value) {
return false;
}
end1 = head1;
end2 = head2;
head1 = head1.next;
head2 = head2.next;
}
if (head1 != null || head2 != null) {
return false;
}
while (end1 != null && end2 != null) {
if (end1.value != end2.value) {
return false;
}
end1 = end1.last;
end2 = end2.last;
}
return end1 == null && end2 == null;
}
public static void main(String[] args) {
int len = 50;
int value = 100;
int testTime = 100000;
for (int i = 0; i < testTime; i++) {
Node node1 = generateRandomLinkedList(len, value);
Node reverse1 = reverseLinkedList(node1);
Node back1 = testReverseLinkedList(reverse1);
if (!checkLinkedListEqual(node1, back1)) {
System.out.println("oops!");
break;
}
DoubleNode node2 = generateRandomDoubleList(len, value);
DoubleNode reverse2 = reverseDoubleList(node2);
DoubleNode back2 = testReverseDoubleList(reverse2);
if (!checkDoubleListEqual(node2, back2)) {
System.out.println("oops!");
break;
}
}
System.out.println("finish!");
}
}
- 把給定的值都刪除
比如給定一個連結串列頭結點,刪除該節點上值為3的節點,那麼可能頭結點就是3,存在刪頭部的情況,這裡最終返回應該是刪除所有值為3的節點之後的新的頭部
package class02;
public class Code02_DeleteGivenValue {
public static class Node {
public int value;
public Node next;
public Node(int data) {
this.value = data;
}
}
// 先檢查頭部,尋找第一個不等於需要刪除的值的節點,就是新的頭部
public static Node removeValue(Node head, int num) {
while (head != null) {
if (head.value != num) {
break;
}
head = head.next;
}
// head來到 第一個不需要刪的位置
Node pre = head;
Node cur = head;
//
while (cur != null) {
if (cur.value == num) {
pre.next = cur.next;
} else {
pre = cur;
}
cur = cur.next;
}
return head;
}
}
Tips: Java中也有可能產生記憶體洩漏,與CPP不同,CPP的記憶體洩漏有可能是我們開闢了記憶體空間忘記釋放。而Java的記憶體洩漏大可能是程式中的變數的生存週期引起的,如果該程式是一個類似定時任務的7*24小時不間斷執行,那麼申請的變數(資料結構)就有可能不會被及時釋放。如果不注意往裡面新增一些不必要的變數,這些變數就是記憶體洩漏
棧、佇列
- 邏輯概念
棧:資料先進後出,猶如彈夾
佇列: 資料先進先出,排隊
- 底層實現方式
雙向連結串列實現
package class02;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;
public class Code03_DoubleEndsQueueToStackAndQueue {
public static class Node<T> {
public T value;
public Node<T> last;
public Node<T> next;
public Node(T data) {
value = data;
}
}
public static class DoubleEndsQueue<T> {
public Node<T> head;
public Node<T> tail;
// 從頭部加節點
public void addFromHead(T value) {
Node<T> cur = new Node<T>(value);
if (head == null) {
head = cur;
tail = cur;
} else {
cur.next = head;
head.last = cur;
head = cur;
}
}
// 從尾部加節點
public void addFromBottom(T value) {
Node<T> cur = new Node<T>(value);
if (head == null) {
head = cur;
tail = cur;
} else {
cur.last = tail;
tail.next = cur;
tail = cur;
}
}
// 從頭部彈出節點
public T popFromHead() {
if (head == null) {
return null;
}
Node<T> cur = head;
if (head == tail) {
head = null;
tail = null;
} else {
head = head.next;
cur.next = null;
head.last = null;
}
return cur.value;
}
// 從尾部彈出節點
public T popFromBottom() {
if (head == null) {
return null;
}
Node<T> cur = tail;
if (head == tail) {
head = null;
tail = null;
} else {
tail = tail.last;
tail.next = null;
cur.last = null;
}
return cur.value;
}
// 該雙向連結串列結構是否為空
public boolean isEmpty() {
return head == null;
}
}
// 用上述雙向連結串列結構實現棧
public static class MyStack<T> {
private DoubleEndsQueue<T> queue;
public MyStack() {
queue = new DoubleEndsQueue<T>();
}
public void push(T value) {
queue.addFromHead(value);
}
public T pop() {
return queue.popFromHead();
}
public boolean isEmpty() {
return queue.isEmpty();
}
}
// 用上述雙向連結串列結構實現佇列
public static class MyQueue<T> {
private DoubleEndsQueue<T> queue;
public MyQueue() {
queue = new DoubleEndsQueue<T>();
}
public void push(T value) {
queue.addFromHead(value);
}
public T poll() {
return queue.popFromBottom();
}
public boolean isEmpty() {
return queue.isEmpty();
}
}
public static boolean isEqual(Integer o1, Integer o2) {
if (o1 == null && o2 != null) {
return false;
}
if (o1 != null && o2 == null) {
return false;
}
if (o1 == null && o2 == null) {
return true;
}
return o1.equals(o2);
}
public static void main(String[] args) {
int oneTestDataNum = 100;
int value = 10000;
int testTimes = 100000;
for (int i = 0; i < testTimes; i++) {
MyStack<Integer> myStack = new MyStack<>();
MyQueue<Integer> myQueue = new MyQueue<>();
Stack<Integer> stack = new Stack<>();
Queue<Integer> queue = new LinkedList<>();
for (int j = 0; j < oneTestDataNum; j++) {
int nums = (int) (Math.random() * value);
if (stack.isEmpty()) {
myStack.push(nums);
stack.push(nums);
} else {
if (Math.random() < 0.5) {
myStack.push(nums);
stack.push(nums);
} else {
if (!isEqual(myStack.pop(), stack.pop())) {
System.out.println("oops!");
}
}
}
int numq = (int) (Math.random() * value);
if (stack.isEmpty()) {
myQueue.push(numq);
queue.offer(numq);
} else {
if (Math.random() < 0.5) {
myQueue.push(numq);
queue.offer(numq);
} else {
if (!isEqual(myQueue.poll(), queue.poll())) {
System.out.println("oops!");
}
}
}
}
}
System.out.println("finish!");
}
}
陣列實現,對於棧特別簡單,對於佇列,如下
package class02;
public class Code04_RingArray {
public static class MyQueue {
// 陣列結構
private int[] arr;
// 往當前佇列新增數的下標位置
private int pushi;
// 當前佇列需要出佇列的位置
private int polli;
// 當前佇列使用的空間大小
private int size;
// 陣列最大大小,使用者傳入
private final int limit;
public MyQueue(int limit) {
arr = new int[limit];
pushi = 0;
polli = 0;
size = 0;
this.limit = limit;
}
public void push(int value) {
if (size == limit) {
throw new RuntimeException("棧滿了,不能再加了");
}
size++;
arr[pushi] = value;
pushi = nextIndex(pushi);
}
public int pop() {
if (size == 0) {
throw new RuntimeException("棧空了,不能再拿了");
}
size--;
int ans = arr[polli];
polli = nextIndex(polli);
return ans;
}
public boolean isEmpty() {
return size == 0;
}
// 如果現在的下標是i,返回下一個位置,該實現可以實現環形的ringbuffer
private int nextIndex(int i) {
return i < limit - 1 ? i + 1 : 0;
}
}
}
棧、佇列常見面試題
一、實現一個特殊的棧,在基本功能的基礎上,再實現返回棧中最小元素的功能更
1、pop、push、getMin操作的時間複雜度都是O(1)
2、設計的棧型別可以使用現成的棧結構
思路:準備兩個棧,一個data棧,一個min棧。資料壓data棧,min棧對比min棧頂元素,誰小加誰。這樣的話data棧和min棧是同步上升的,元素個數一樣多,且min棧的棧頂,是data棧所有元素中最小的那個。資料彈出data棧,我們同步彈出min棧,保證個數相等,切min棧彈出的就是最小值
package class02;
import java.util.Stack;
public class Code05_GetMinStack {
public static class MyStack1 {
private Stack<Integer> stackData;
private Stack<Integer> stackMin;
public MyStack1() {
this.stackData = new Stack<Integer>();
this.stackMin = new Stack<Integer>();
}
public void push(int newNum) {
// 當前最小棧為空,直接壓入
if (this.stackMin.isEmpty()) {
this.stackMin.push(newNum);
// 當前元素小於最小棧的棧頂,壓入當前值
} else if (newNum <= this.getmin()) {
this.stackMin.push(newNum);
}
// 往資料棧中壓入當前元素
this.stackData.push(newNum);
}
public int pop() {
if (this.stackData.isEmpty()) {
throw new RuntimeException("Your stack is empty.");
}
int value = this.stackData.pop();
if (value == this.getmin()) {
this.stackMin.pop();
}
return value;
}
public int getmin() {
if (this.stackMin.isEmpty()) {
throw new RuntimeException("Your stack is empty.");
}
return this.stackMin.peek();
}
}
public static class MyStack2 {
private Stack<Integer> stackData;
private Stack<Integer> stackMin;
public MyStack2() {
this.stackData = new Stack<Integer>();
this.stackMin = new Stack<Integer>();
}
public void push(int newNum) {
if (this.stackMin.isEmpty()) {
this.stackMin.push(newNum);
} else if (newNum < this.getmin()) {
this.stackMin.push(newNum);
} else {
int newMin = this.stackMin.peek();
this.stackMin.push(newMin);
}
this.stackData.push(newNum);
}
public int pop() {
if (this.stackData.isEmpty()) {
throw new RuntimeException("Your stack is empty.");
}
// 彈出操作,同步彈出,保證大小一致,只返回給使用者data棧中的內容即可
this.stackMin.pop();
return this.stackData.pop();
}
public int getmin() {
if (this.stackMin.isEmpty()) {
throw new RuntimeException("Your stack is empty.");
}
return this.stackMin.peek();
}
}
public static void main(String[] args) {
MyStack1 stack1 = new MyStack1();
stack1.push(3);
System.out.println(stack1.getmin());
stack1.push(4);
System.out.println(stack1.getmin());
stack1.push(1);
System.out.println(stack1.getmin());
System.out.println(stack1.pop());
System.out.println(stack1.getmin());
System.out.println("=============");
MyStack1 stack2 = new MyStack1();
stack2.push(3);
System.out.println(stack2.getmin());
stack2.push(4);
System.out.println(stack2.getmin());
stack2.push(1);
System.out.println(stack2.getmin());
System.out.println(stack2.pop());
System.out.println(stack2.getmin());
}
}
二、如何用棧結構實現佇列結構,如何用佇列結構實現棧結構
這兩種結構的應用實在太多,刷題時會大量見到
/**
* 兩個棧實現佇列
**/
package class02;
import java.util.Stack;
public class Code06_TwoStacksImplementQueue {
public static class TwoStacksQueue {
public Stack<Integer> stackPush;
public Stack<Integer> stackPop;
public TwoStacksQueue() {
stackPush = new Stack<Integer>();
stackPop = new Stack<Integer>();
}
// push棧向pop棧倒入資料
private void pushToPop() {
if (stackPop.empty()) {
while (!stackPush.empty()) {
stackPop.push(stackPush.pop());
}
}
}
public void add(int pushInt) {
stackPush.push(pushInt);
pushToPop();
}
public int poll() {
if (stackPop.empty() && stackPush.empty()) {
throw new RuntimeException("Queue is empty!");
}
pushToPop();
return stackPop.pop();
}
public int peek() {
if (stackPop.empty() && stackPush.empty()) {
throw new RuntimeException("Queue is empty!");
}
pushToPop();
return stackPop.peek();
}
}
public static void main(String[] args) {
TwoStacksQueue test = new TwoStacksQueue();
test.add(1);
test.add(2);
test.add(3);
System.out.println(test.peek());
System.out.println(test.poll());
System.out.println(test.peek());
System.out.println(test.poll());
System.out.println(test.peek());
System.out.println(test.poll());
}
}
/**
* 兩個佇列實現棧
**/
package class02;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;
public class Code07_TwoQueueImplementStack {
public static class TwoQueueStack<T> {
public Queue<T> queue;
public Queue<T> help;
public TwoQueueStack() {
queue = new LinkedList<>();
help = new LinkedList<>();
}
public void push(T value) {
queue.offer(value);
}
public T poll() {
while (queue.size() > 1) {
help.offer(queue.poll());
}
T ans = queue.poll();
Queue<T> tmp = queue;
queue = help;
help = tmp;
return ans;
}
public T peek() {
while (queue.size() > 1) {
help.offer(queue.poll());
}
T ans = queue.poll();
help.offer(ans);
Queue<T> tmp = queue;
queue = help;
help = tmp;
return ans;
}
public boolean isEmpty() {
return queue.isEmpty();
}
}
public static void main(String[] args) {
System.out.println("test begin");
TwoQueueStack<Integer> myStack = new TwoQueueStack<>();
Stack<Integer> test = new Stack<>();
int testTime = 1000000;
int max = 1000000;
for (int i = 0; i < testTime; i++) {
if (myStack.isEmpty()) {
if (!test.isEmpty()) {
System.out.println("Oops");
}
int num = (int) (Math.random() * max);
myStack.push(num);
test.push(num);
} else {
if (Math.random() < 0.25) {
int num = (int) (Math.random() * max);
myStack.push(num);
test.push(num);
} else if (Math.random() < 0.5) {
if (!myStack.peek().equals(test.peek())) {
System.out.println("Oops");
}
} else if (Math.random() < 0.75) {
if (!myStack.poll().equals(test.pop())) {
System.out.println("Oops");
}
} else {
if (myStack.isEmpty() != test.isEmpty()) {
System.out.println("Oops");
}
}
}
}
System.out.println("test finish!");
}
}
遞迴
1、從思想上理解遞迴
2、從實現角度出發理解遞迴
例子:
求陣列arr[L...R]中的最大值,怎麼用遞迴方法實現
1、 將[L...R]範圍分成左右兩半。左[L...Mid],右[Mid+1...R]
2、 左部分求最大值,右部分求最大值
3、[L...R]範圍上的最大值,就是max{左部分最大值,右部分最大值}
2步驟是個遞迴過程,當範圍上只有一個數,就可以不用再遞迴了
package class02;
public class Code08_GetMax {
// 求arr中的最大值
public static int getMax(int[] arr) {
return process(arr, 0, arr.length - 1);
}
// arr[L..R]範圍上求最大值 L ... R N
public static int process(int[] arr, int L, int R) {
if (L == R) { // arr[L..R]範圍上只有一個數,直接返回,base case
return arr[L];
}
int mid = L + ((R - L) >> 1); // 中點
// 左部分最大值
int leftMax = process(arr, L, mid);
// 右部分最大值
int rightMax = process(arr, mid + 1, R);
return Math.max(leftMax, rightMax);
}
}
遞迴在系統中是怎麼實現的?遞迴實際上利用的是系統棧來實現的。儲存當前呼叫現場,去執行子問題,子問題的返回作為現場的需要的引數填充,最終構建還原棧頂的現場,返回。所以遞迴行為不是玄學,任何遞迴都可以改為非遞迴實現,我們自己壓棧用迭代等實現就行
遞迴行為的時間複雜度
對於滿足
T(N) = aT(N/b) + O(N^d)
其中: a,b,d為常數
公式表示,子問題的規模是一致的,該子問題呼叫了a次,N/b代表子問題的規模,O(N^d)為除去遞迴呼叫剩餘的時間複雜度。
比如上述問題的遞迴,[L...R]上有N個數,第一個子問題的規模是N/2,第二個子問題的規模也是N/2。子問題呼叫了2次。額為複雜度為O(1),那麼公式為:
T(N) = 2T(N/2) + O(N^0)
結論:如果我們的遞迴滿足這種公式,那麼該遞迴的時間複雜度(Master公式)為
logb^a > d => O(N ^ (logb^a))
logb^a < d => O(N^d)
logb^a == d => O(N^d * logN)
那麼上述問題的a=2, b=2,d=0,滿足第一條,遞迴時間複雜度為:O(N)
雜湊表HashMap、HashSet
Hash表的增刪改查,在使用的時候,一律認為時間複雜度是O(1)的
在Java中,int double float基礎型別,按值傳遞; Integer, Double, Float按引用傳遞的,比較包裝型別的值是否相等,使用equals方法。
注意:在Java底層,包裝類如果範圍比較小,底層仍然採用值傳遞,比如Integer如果範圍在-128~127之間,是按值傳遞的
但是在Hash表中,即使是包裝型別的key,我們也一律按值傳遞,例如Hash<Integer,String>如果我們put相同的key的值,那麼不會產生兩個值相等的key而是覆蓋操作。但是Hash表並不是一直是按值傳遞的,只是針對包裝型別,如果是我們自定義的引用型別,那麼仍然按引用傳遞
順序表 TreeMap、TreeSet
順序表比雜湊表功能多,但是順序表的很多操作時間複雜度是O(logN)
有序表的底層可以有很多結構實現,比如AVL樹,SB樹,紅黑樹,跳錶。其中AVL,SB,紅黑都是具備各自平衡性的搜尋二叉樹
由於平衡二叉樹每時每刻都會維持自身的平衡,所以操作為O(logN)。暫時理解,後面會單獨整理
由於滿足去重排序功能來維持底層樹的平衡,所以如果是基礎型別和包裝型別的key直接按值來做比較,但是如果我們的key是自己定義的型別,那麼我們要自己制定比較規則(比較器),用來讓底層的樹保持比較後的平衡
package class02;
import java.util.HashMap;
import java.util.HashSet;
import java.util.TreeMap;
public class HashMapAndSortedMap {
public static class Node{
public int value;
public Node(int v) {
value = v;
}
}
public static void main(String[] args) {
// UnSortedMap
HashMap<Integer, String> map = new HashMap<>();
map.put(1000000, "我是1000000");
map.put(2, "我是2");
map.put(3, "我是3");
map.put(4, "我是4");
map.put(5, "我是5");
map.put(6, "我是6");
map.put(1000000, "我是1000001");
System.out.println(map.containsKey(1));
System.out.println(map.containsKey(10));
System.out.println(map.get(4));
System.out.println(map.get(10));
map.put(4, "他是4");
System.out.println(map.get(4));
map.remove(4);
System.out.println(map.get(4));
// key
HashSet<String> set = new HashSet<>();
set.add("abc");
set.contains("abc");
set.remove("abc");
// 雜湊表,增、刪、改、查,在使用時,O(1)
System.out.println("=====================");
int a = 100000;
int b = 100000;
System.out.println(a == b);
Integer c = 100000;
Integer d = 100000;
System.out.println(c.equals(d));
Integer e = 127; // - 128 ~ 127
Integer f = 127;
System.out.println(e == f);
HashMap<Node, String> map2 = new HashMap<>();
Node node1 = new Node(1);
Node node2 = node1;
map2.put(node1, "我是node1");
map2.put(node2, "我是node1");
System.out.println(map2.size());
System.out.println("======================");
TreeMap<Integer, String> treeMap = new TreeMap<>();
treeMap.put(3, "我是3");
treeMap.put(4, "我是4");
treeMap.put(8, "我是8");
treeMap.put(5, "我是5");
treeMap.put(7, "我是7");
treeMap.put(1, "我是1");
treeMap.put(2, "我是2");
System.out.println(treeMap.containsKey(1));
System.out.println(treeMap.containsKey(10));
System.out.println(treeMap.get(4));
System.out.println(treeMap.get(10));
treeMap.put(4, "他是4");
System.out.println(treeMap.get(4));
treeMap.remove(4);
System.out.println(treeMap.get(4));
System.out.println(treeMap.firstKey());
System.out.println(treeMap.lastKey());
// <= 4
System.out.println(treeMap.floorKey(4));
// >= 4
System.out.println(treeMap.ceilingKey(4));
// O(logN)
}
}