前言
- 从物理存储角度来看,只有两种结构:数组结构和链表结构
- 数组结构:就是在内存中开辟一块连续的空间,各个元素排排坐,不能超过开辟的空间长度;
- 链表结构:不预先开辟空间,各个元素不一定连续挨着,但是每个元素都有它下一个元素的"地址",通过地址找到下一位,这就是(单向)链表,像链条一样 一个链一个;
- 其他的像栈,队列,树都是逻辑上的结构
链表结构(物理结构)
- 单向链表
一个链表中,每个节点有且只有它的下一个节点的引用
public class Node {
public int value;
public Node next;
public Node(int data) {
value = data;
}
}
- 双向链表
一个链表中,每个节点有它的下一个节点的引用,也有它的上一个节点的引用
public class DoubleNode {
public int value;
public DoubleNode last;
public DoubleNode next;
public DoubleNode(int data) {
value = data;
}
}
- 单链表如何反转
public class Node {
private int value;
private Node next;
public Node(int value) {
this.value = value;
}
}
//单链表反转
private void One(Node head) {
Node pre = null;
Node next = null;
while (head != null) {
//记录位置
next = head.next;
//反向
head.next = pre;
pre = head;
head = next;
}
return;
}
- 双链表如何反转
public class DoubleNode {
private int value;
private DoubleNode next;
private DoubleNode last;
public DoubleNode(int value) {
this.value = value;
}
}
//双链表反转
private void Two(DoubleNode head) {
DoubleNode next = null;
DoubleNode pre = null;
while (head != null) {
next = head.next;
head.next = pre;
head.next = next;
pre = head;
head = next;
}
return;
}
- 单链表,删除指定值
public class Node {
private int value;
private Node next;
public Node(int value) {
this.value = value;
}
}
private void three(Node head, int value) {
//先删除头
while (head != null) {
if (head.value == value) {
head = head.next;
} else break;
}
//删除后面的
Node pre = head;
Node next = head;
while (next != null) {
if (next.value == value) {
pre.next = head.next;
} else {
pre = next;
}
next = next.next;
}
}
- 双链表,删除指定值
public class DoubleNode {
private int value;
private DoubleNode next;
private DoubleNode last;
public DoubleNode(int value) {
this.value = value;
}
}
private void Four(DoubleNode head, int value) {
while (head != null) {
if (head.value == value) {
head = head.next;
head.last = null;
} else break;
}
DoubleNode pre = null;
DoubleNode cur = null;
while (cur != null) {
if (cur.value == value) {
pre.next = cur.next;
pre = cur.next.last;
} else {
pre = cur;
}
cur = cur.next;
}
}
栈和队列(逻辑结构)
栈和队列是逻辑上的结构,一般都是由双向链表实现
- 栈:先进后出, 想象一下汉诺塔,弹夹
- 队列:先进先出,FIFO, 想象一下超市排队结账
用双向链表实现栈和队列
核心是通过一个双向链表实现的,可以headPush,headPop,tailPush,tailPop
核心-双向链表
public class DoubleEndsQueue<E> {
public class DoubleNode2<E> {
private E value;
private DoubleNode2<E> next;
private DoubleNode2<E> last;
public DoubleNode2(E value) {
this.value = value;
}
public E getValue() {
return value;
}
public void setValue(E value) {
this.value = value;
}
public DoubleNode2<E> getNext() {
return next;
}
public void setNext(DoubleNode2<E> next) {
this.next = next;
}
public DoubleNode2<E> getLast() {
return last;
}
public void setLast(DoubleNode2<E> last) {
this.last = last;
}
}
private DoubleNode2<E> head;
private DoubleNode2<E> tail;
//插入头部
public void pushHead(E value) {
DoubleNode2<E> node2 = new DoubleNode2<E>(value);
if (head == null) {
head = node2;
tail = node2;
return;
}
node2.setNext(head);
}
//插入尾部
public void pushTail(E value) {
DoubleNode2<E> node2 = new DoubleNode2<E>(value);
if (tail == null) {
head = node2;
tail = node2;
return;
}
tail.setNext(node2);
node2.setLast(tail);
tail = node2;
}
//弹出头部
public E popHead() {
if (head == null) {
return null;
}
DoubleNode2<E> cur = head;
if (head == tail) {
head = null;
tail = null;
} else {
head = head.getNext();
head.setLast(null);
cur.setNext(null);
}
return cur.getValue();
}
public E popTail() {
if (tail == null) {
return null;
}
DoubleNode2<E> cur = tail;
if (tail == head) {
tail = null;
head = null;
} else {
tail = tail.getLast();
tail.setNext(null);
cur.setLast(null);
}
return cur.getValue();
}
}
- 栈
//用双向链表实现栈
//后进后出
public class MyStack<E> {
DoubleEndsQueue<E> linkedList = new DoubleEndsQueue<>();
public void push(E value) {
linkedList.pushHead(value);
}
public E pop() {
return linkedList.popHead();
}
}
- 队列
//用双向链表实现队列
//主要先进先出
public class MyQueue<E> {
private DoubleEndsQueue<E> linkedList = new DoubleEndsQueue<>();
public void offer(E value) {
linkedList.pushHead(value);
}
public E poll() {
return linkedList.popTail();
}
}
用数组实现栈和队列
- 栈
比较简单,直接维护一个offset就好
public class MyStack2<E> {
Object[] arr;
int size;
int offer = -1;
public void push(E element) {
if (offer == size) return;
arr[++offer] = element;
}
public E pop() {
if (offer < 0) {
return null;
}
E e = (E) arr[offer--];
return e;
}
public MyStack2(int size) {
this.size = size;
arr = new Object[size];
}
}
- 队列
要考虑ring buffer的问题了,如果通过head和tail"追赶"的方式实现,略显复杂;
添加一个变量length表示当前队列中有多少个元素,就简单很多
public class MyQueue2<E> {
Object[] arr;
int size;
int length;
int addIndex;
int popIndex;
public boolean offer(E element) {
if (size == length) return false;
length++;
arr[addIndex] = element;
addIndex = nextIndex(addIndex);
return true;
}
public E poll() {
if (length == 0) {
return null;
}
length--;
E e = (E) arr[popIndex];
// popIndex++
popIndex = nextIndex(popIndex);
return e;
}
private int nextIndex(int index) {
return index < size - 1 ? index + 1 : 0;
}
public MyQueue2(int size) {
this.size = size;
arr = new Object[size];
}
}
面试题
一、实现一个特殊的栈,在基本功能的基础上,再实现返回栈中最小元素的功能更
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());
}
}
二、用俩栈实现队列
思路:(倒一次)用两个栈,一个pushStack负责push,一个popStack负责pop当pop的时候如果popStack为空则倒腾一次,把pushStack一个个弹入popStack
/**
* 两个栈实现队列
**/
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());
}
}
三、用俩队列实现栈
思路:用两个队列,dataQ和helpQ 每次pop时倒腾一次,把dataQ[1~N]一个个弹入helpQ,留下dataQ[0]等会返回不入helpQ,然后交换dataQ和helpQ的引用
/**
* 两个队列实现栈
**/
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!");
}
}
递归
-
递归思想:
递归就是把一个大任务,不断的切分成几个子任务,
然后每个子任务又切分成孙子任务…
直到达到某个条件后,不再继续向下切分,向上返回. -
举个例子说明什么是递归:
求数组arr[L…R]中的最大值,怎么用递归方法实现
//递归获取数组最大值
public static int getMax(int[] arr) {
if (arr.length == 0) return Integer.parseInt(null);
return process(arr,0,arr.length-1);
}
private static int process(int[] arr, int L, int R) {
if (L==R)return arr[L];
int mid = L + ((R - L) >> 1); // 中点
int maxL=process(arr,L,mid);
int maxR=process(arr,mid+1,R);
return Math.max(maxL,maxR);
}
- 递归的时间复杂度Master公式:
对于满足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)
哈希表
Hash表的增删改查,在使用的时候,一律认为时间复杂度是O(1)的
注意:在Java底层,包装类如果范围比较小,底层仍然采用值传递,比如Integer如果范围在-128~127之间,是按值传递的
但是在Hash表中,即使是包装类型的key,我们也一律按值传递,例如Hash<Integer,String>如果我们put相同的key的值,那么不会产生两个值相等的key而是覆盖操作。但是Hash表并不是一直是按值传递的,只是针对包装类型,如果是我们自定义的引用类型,那么仍然按引用传递
有序表
顺序表比哈希表功能多,但是顺序表的很多操作时间复杂度是O(logN)
有序表的底层可以有很多结构实现,比如AVL树,SB树,红黑树,跳表。其中AVL,SB,红黑都是具备各自平衡性的搜索二叉树
由于平衡二叉树每时每刻都会维持自身的平衡,所以操作为O(logN)。暂时理解,后面会单独整理
由于满足去重排序功能来维持底层树的平衡,所以如果是基础类型和包装类型的key直接按值来做比较,但是如果我们的key是自己定义的类型,那么我们要自己制定比较规则(比较器),用来让底层的树保持比较后的平衡