栈的特性:先进后出,后进先出
队列的特性: 先进先出,后进后出
图的宽度有限遍历用栈来实现 => 相当于用栈实现队列
先上代码
/**
* 用于栈实现队列
* @author hanhuafeng
* @version V1.0
* @description
* @date 2020/11/15
*/
public class Code06_TwoStacksImplementQueue {
/**
* 先定义两个栈,模拟出系统给出的栈方法
*/
private Stack<Integer> stackPush;
private Stack<Integer> stackPop;
/**
* 构造方法构建
*/
public Code06_TwoStacksImplementQueue(){
stackPush = new Stack<>();
stackPop = new Stack<>();
}
/**
* 将stackPush栈中的数据倒入StackPop栈中
* 实现了把stackPush栈中的数据重新倒序
* 此方法将会被用在pop、push、peak方法中
*/
public void importStackPushDataToStackPop(){
if (stackPop.isEmpty()){
while (!stackPush.isEmpty()){
stackPop.push(stackPush.pop());
}
}
}
/**
* 实现队列add的方式
* 每次都会去检查一次stackPop栈是否为空,如果为空则把数据全部吐进去
* @param i 用户给定的入队列的参数
*/
public void add(Integer i){
stackPush.push(i);
importStackPushDataToStackPop();
}
/**
* 实现队列poll方法
* 每次都会去检查一次stackPop栈是否为空,如果为空则把数据全部吐进去
* @return
*/
public Integer poll(){
// 两个栈是否都是空的,如果都是空的,则直接提示用户
if(stackPush.isEmpty() && stackPop.isEmpty()){
throw new RuntimeException("stack is empty");
}
importStackPushDataToStackPop();
return stackPop.pop();
}
/**
* 实现队列的peak方法
* 从队列头部删除一个元素
* @return
*/
public Integer peek(){
// 两个栈是否都是空的,如果都是空的,则直接提示用户
if(stackPush.isEmpty() && stackPop.isEmpty()){
throw new RuntimeException("stack is empty");
}
importStackPushDataToStackPop();
return stackPop.peek();
}
public static void main(String[] args) {
Code06_TwoStacksImplementQueue test = new Code06_TwoStacksImplementQueue();
test.add(12);
test.add(11);
test.add(10);
test.add(9);
test.add(8);
Queue<Integer> queue = new LinkedList();
queue.add(12);
queue.add(11);
queue.add(10);
queue.add(9);
queue.add(8);
System.out.println(test.poll().equals(queue.poll()));
System.out.println(test.poll().equals( queue.poll()));
System.out.println(test.peek().equals(queue.peek()));
}
}
实现思路:
栈的特性为【先进后出,后进先出】,而队列的特性为【先进先出,后进后出】,这两者相当于反了一下,所以采用两个栈,相互照应,就可以实现出负负得正的效果。
两个栈,一个为push栈,一个为pop栈,push栈用于接收用户输入的数值,pop栈用于输出数据
每一次执行push、pop和week操作的时候,都去判断一次pop栈是否为空,如果为空才能把push栈中的数据全部压入pop栈,具体可看下图。
如图,把4,7,6压入右边的posStack中,此时会造成的记过便是:理论上下一位需要poll出的是2,而因为popStack没有清空便压入,造成了poll出了最后一位6
当然,pushStack栈需要把所有的数据一次性压入,否则会造成数据错乱,具体看下图
此时如果用户又新增了一个数字,例如3,导致pushStack变成如下的样子
所以又造成了数据的错乱,理论上要poll出4,但是poll出了3
图的深度有限遍历用队列实现 => 相当于用队列实现栈
先上代码:
/**
* 题目:给定队列的一切方法,如何实现栈
* 具体思路:
* 通过两个队列互相倒入数据达到实现栈的目的
* @author hanhuafeng
* @version V1.0
* @description
* @date 2020/11/15
*/
public class Code_TwoQueuesImplementStack {
/**
* 先定义两个队列,模拟出系统给出的队列方法
*/
private Queue<Integer> pushQueue;
private Queue<Integer> popQueue;
/**
* 构造方法构建
*/
public Code_TwoQueuesImplementStack(){
pushQueue = new LinkedList<>();
popQueue = new LinkedList<>();
}
/**
* 先把数据压入pushQueue中
* 再把popQueue中的全部数据压入pushQueue
* 再把pushQueue重新压回popQueue
* 此方法将会被用在pop、push、peak方法中
*/
public void importStackPushDataToStackPop(){
if (!popQueue.isEmpty()){
while (!popQueue.isEmpty()){
pushQueue.add(popQueue.poll());
}
}
while (!pushQueue.isEmpty()){
popQueue.add(pushQueue.poll());
}
}
/**
* 实现队列add的方式
* 每次都会去检查一次stackPop栈是否为空,如果为空则把数据全部吐进去
* @param i 用户给定的入队列的参数
*/
public void push(Integer i){
pushQueue.add(i);
importStackPushDataToStackPop();
}
/**
* 实现栈的出栈pop方法
* 如果有数据,则直接吐就行了
* @return
*/
public Integer pop(){
// 检测pop队列是否是空的,如果是空的,则直接提示用户
if(popQueue.isEmpty()){
throw new RuntimeException("stack is empty");
}
return popQueue.poll();
}
/**
* 实现栈的peak方法
* 从队列头部弹出一个元素,不删除
* @return
*/
public Integer peek(){
// 两个栈是否都是空的,如果都是空的,则直接提示用户
if(popQueue.isEmpty()){
throw new RuntimeException("stack is empty");
}
return popQueue.peek();
}
public static void main(String[] args) {
Code_TwoQueuesImplementStack test = new Code_TwoQueuesImplementStack();
test.push(12);
test.push(11);
test.push(10);
test.push(9);
test.push(8);
Stack<Integer> stack = new Stack<>();
stack.push(12);
stack.push(11);
stack.push(10);
stack.push(9);
stack.push(8);
System.out.println(test.pop().equals(stack.pop()));
System.out.println(test.pop().equals( stack.pop()));
System.out.println(test.pop().equals( stack.pop()));
System.out.println(test.pop().equals( stack.pop()));
System.out.println(test.peek().equals(stack.peek()));
}
}
具体的实现思路请看下面的解释图
数据只会从popQueue中取出,
pushQueue只负责了用户数据一开始的压入
以及数据从popQueue重新导回来再把全部数据导出只popQueue。
单链表的实现,实现单链表的反转和删除
先上代码:
/**
* 实现单向链表
* 链表的特性为当前节点存有下一节点的地址信息
* 本代码主要是实现单链表的反转和删除
* @author hanhuafeng
* @version V1.0
* @date 2020/11/19
*/
public class Code_SingleLinkedList {
public static class Node {
private int value;
private Node next;
private Node(int data) {
value = data;
}
private int getValue() {
return value;
}
private Node getNext() {
return next;
}
private void setNext(Node next) {
this.next = next;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
", next=" + next +
'}';
}
}
/**
* 反转的方法
* 用户给出头部的Node节点,通过指针替换的方式,进行反转
* @author hanhuafeng
* @date : 22:10 2020/11/19
* @param head 头部指针
* @return 返回已经反转好的链表
*/
public static Node reverse(Node head){
Node pre = null;
Node next;
while (head !=null){
next = head.getNext();
head.setNext(pre);
pre = head;
head = next;
}
return pre;
}
/**
* 删除链表中指定的node节点
* 需要考虑头节点被删除的情况,所以需要先遍历出头节点
* @author: hanhuafeng
* @date : 22:45 2020/11/19
* @param value 待删除的值
* @return 返回已经删除完毕的链表
*/
private static Node deleteNode(Node head, int value){
while (head !=null){
if(head.getValue() != value){
break;
}
head = head.getNext();
}
Node now = head;
Node pre = head;
while (now != null){
if(now.getValue() == value){
pre.setNext(now.getNext());
}
pre = now;
now = now.getNext();
}
return head;
}
//对数器验证
/**
* 生成一个随机的单链表
* @return
*/
public static Node randomNode(int maxSize){
// 生成一个链表长度
int size = (int)(Math.random()*maxSize)+1;
// 设置链表中最大的数据为10000
int maxValue = 10000;
Node[] nodes = new Node[size];
for(int i = 0;i<size;i++){
nodes[i] = new Node((int)(Math.random()*maxValue)+1);
}
for (int i =0;i<nodes.length-1;i++){
nodes[i].setNext(nodes[i+1]);
}
return nodes[0];
}
/**
* 使用java原生的方法做一个倒序比较器
* @param node 需要倒序的node
* @return
*/
private static List<Integer> testReverse(Node node){
// 直接使用java自带的LinkedList
List<Integer> list = new LinkedList<>();
while (node !=null){
list.add(node.getValue());
node = node.getNext();
}
// 1.8自带的倒序方法
Collections.reverse(list);
return list;
}
/**
* 使用java原生的方法做一个删除比较器
* @param node 需要删除的
* @return
*/
private static List<Integer> testDelete(Node node,int value){
// 直接使用java自带的LinkedList
List<Integer> list = new LinkedList<>();
while (node !=null){
list.add(node.getValue());
node = node.getNext();
}
// 1.8自带的删除方法
list.removeIf(integer -> integer == value);
return list;
}
public static void main(String[] args) throws CloneNotSupportedException {
// 最大测试次数
int maxTestSize = 10000;
int maxSize = 1000;
for(int i =0;i<maxTestSize;i++){
Node node1 = randomNode((int)(Math.random()*maxSize)+1);
// 用系统方式实现的倒序
List<Integer> integers = testReverse(node1);
// 自己写的倒序方式
node1 = reverse(node1);
int index = 0;
while (node1.getNext() != null){
if(node1.getValue() != integers.get(index++)){
System.out.println("出错了");
break;
}
node1 = node1.getNext();
}
}
for(int i =0;i<maxTestSize;i++){
Node node1 = randomNode((int)(Math.random()*maxSize)+1);
int deleteNum = (int)(Math.random()*maxSize)+1;
// 用系统方式实现的倒序
List<Integer> integers = testDelete(node1,deleteNum);
// 自己写的倒序方式
node1 = deleteNode(node1,deleteNum);
int index = 0;
while (node1.getNext() != null){
if(node1.getValue() != integers.get(index++)){
System.out.println("出错了");
break;
}
node1 = node1.getNext();
}
}
}
}
先解释一下什么是链表,上图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RdzLmNTm-1606654569491)(https://raw.githubusercontent.com/hanhuafeng/pic_factory/master/20201120233859.png)]
图中的Value就是这个节点中的存的数据,Address就是记录了下一个节点的地址
例如,地址为A的Node中的Address记录了地址为B的地址反转代码具体的实现思路请看下面的解释:
首先是第一步
接下来把pre,head,next往下移动
一直需要移动到以下情况才能结束,最后把图中的pre返回出去,当成新的头部即可
对应代码如下:
Node pre = null;
Node next;
while (head !=null){
next = head.getNext();
head.setNext(pre);
pre = head;
head = next;
}
return pre;
删除链表中指定的node节点解释:
遍历到可以使用的头部代码:
while (head !=null){
if(head.getValue() != value){
break;
}
head = head.getNext();
}
/**
为什么要上这块代码?
首先需要保证能找到一个可用的头,例如以下的情况:
链表中的值如下情况,需要删除1最后的返回情况是怎么样的?
1,2,3,4,1,5,6
最后的返回:2,3,4,5,6
可以看出需要把2作为新的头部作为返回结果,所以开头就需要一个while去找到可用的头部
接下来就是正常的流程了,代码对应的具体流程请看下图:
*/
if(now.getValue() == value){
pre.setNext(now.getNext());
}
pre = now;
now = now.getNext();
双链表的实现,实现双向链表的反转以及双向链表节点的删除
先上代码:
/**
* 双链表的实现
* 实现双向链表的反转以及双向链表节点的删除
* @author hanhuafeng
* @date 2020/11/19
*/
public class Code_DoubleLinkedList {
public static class DoubleNode {
private int value;
private DoubleNode pre;
private DoubleNode next;
public DoubleNode(int data) {
value = data;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public DoubleNode getPre() {
return pre;
}
public void setPre(DoubleNode pre) {
this.pre = pre;
}
public DoubleNode getNext() {
return next;
}
public void setNext(DoubleNode next) {
this.next = next;
}
}
/**
* 反转的方法
* 用户给出头部的Node节点,通过指针替换的方式,进行反转
* @author hanhuafeng
* @date : 22:10 2020/11/19
* @param head 头部指针
* @return 返回已经反转好的链表
*/
public static DoubleNode reverse(DoubleNode head){
DoubleNode pre = null;
DoubleNode next;
DoubleNode preIndex=null;
while (head !=null){
pre = head.getPre();
next = head.getNext();
head.setPre(next);
head.setNext(pre);
// preIndex因为最后会遍历到null为止,所以要记录一下null之前的节点用于返回
preIndex= head;
head = next;
}
return preIndex;
}
/**
* 删除链表中指定的node节点
* 需要考虑头节点被删除的情况,所以需要先遍历出头节点
* @author: hanhuafeng
* @date : 22:45 2020/11/19
* @param value 待删除的值
* @return 返回已经删除完毕的链表
*/
private static DoubleNode deleteNode(DoubleNode head, int value){
while (head !=null){
if(head.getValue() != value){
break;
}
head = head.getNext();
head.setPre(null);
}
DoubleNode now = head;
DoubleNode pre;
DoubleNode next;
while (now != null){
pre = head.getPre();
next = head.getNext();
if(now.getValue() == value){
if(pre!=null){
pre.setNext(next);
}
if(next!=null){
next.setPre(pre);
}
}
now = now.getNext();
}
return head;
}
public static void main(String[] args) {
DoubleNode doubleNode1 = new DoubleNode(1);
DoubleNode doubleNode2 = new DoubleNode(2);
DoubleNode doubleNode3 = new DoubleNode(3);
doubleNode1.setNext(doubleNode2);
doubleNode2.setPre(doubleNode1);
doubleNode2.setNext(doubleNode3);
doubleNode3.setPre(doubleNode2);
doubleNode1 = reverse(doubleNode1);
doubleNode1 = deleteNode(doubleNode1,3);
}
}
双向链表的实现比单向链表简单,双向链表的翻转只是将pre节点和next交换位置即可实现,
具体的的实现逻辑请看下图:
实现从一个栈中取最小值GetMin,要求时间复杂度O(1)
先上代码:
/**
* 实现从一个栈中取最小值GetMin,和实现栈的pop和push方法,要求时间复杂度O(1)
* 解析:
* 要求了时间复杂度为1,即说明了不能使用遍历的方式取出最小值
* 所以这边可以采用两个栈,一个用于正常push数据,一个用于每次把最小值压入,并且pop出最小值
* 每次有数据进来都检测一次pop栈的栈顶数据是否是最小的,如果是最小的pop栈依旧压入栈顶的数据,否则压入刚进来的数据
* @author hanhuafeng
* @version V1.0
* @description
* @date 2020/11/18
*/
public class Code_StackGetMin {
private Stack<Integer> pushStack;
private Stack<Integer> minStack;
public Code_StackGetMin(){
pushStack = new Stack<>();
minStack = new Stack<>();
}
public void push(int val){
if(minStack.empty()){
minStack.push(val);
}else{
minStack.push(val<minStack.peek()?val:minStack.peek());
}
pushStack.push(val);
}
public int pop(){
if (pushStack.empty()){
throw new RuntimeException("栈还是空的!");
}
this.minStack.pop();
return pushStack.pop();
}
public int getMin(){
if(pushStack.empty()){
throw new RuntimeException("栈还是空的!");
}
return this.minStack.peek();
}
public static void main(String[] args) {
Code_StackGetMin stackGetMin = new Code_StackGetMin();
stackGetMin.push(1);
stackGetMin.push(2);
stackGetMin.push(3);
stackGetMin.push(4);
stackGetMin.push(0);
System.out.println(stackGetMin.getMin());
System.out.println(stackGetMin.pop());
System.out.println(stackGetMin.getMin());
}
}
最后的数据结构如下图:
递归
PS:任何递归的代码一定都可以改成非递归!
递归在系统中的简单实现
/**
* 用递归的方式在arr中求出最大值
*
* @author hanhuafeng
* @date 2020/11/21
*/
public class Code_RecursionImplentGetMax {
public static int getMax(int[]arr) {
return getMax(arr,0,arr.length-1);
}
public static int getMax(int[] arr, int l, int r) {
if (l == r) {
// 说明数组中只存在了一个数字,返回任意一个都行
return arr[l];
}
// 求中点
int mid = l + ((r - l) >> 1);
int leftMax = getMax(arr,l,mid);
int rightMax = getMax(arr,mid+1,r);
return leftMax>rightMax?leftMax:rightMax;
}
}
递归最需要考虑的是baseCase,即何时结束
Master 公式(用于估算递归行为的时间复杂度)
形如:T(N) = a*T(N/b) + O(N^d)(其中的 a、b、d 都是常数)的递归函数,可以直接通过 Master 公式来确定时间复杂度
如果 log(b,a) < d,复杂度为 O(N^d)
如果 log(b,a) > d,复杂度为 O(N^log(b,a))
如果 log(b,a) == d,复杂度为 O(N^d * logN)
a(子过程调用了几次)
T(N/b) 子过程数据规模
Master公式运用的条件:
PS:master针对于递归函数并且子规模一致的情况下适用
哈希表
- 哈希表的增删改查操作的时间复杂度都是常数时间,只是这个常数会比较大,这类操作包括了containsKey、get、put等操作
- 如果Hashmap<String,String>,hash表内部会存储字符串,而不是这个字符串的地址,但如果定义的是Hashmap<Node,Node>,hash表内部会存储node的内存地址,只占用8字节;
- HashMap和HashSet性能都是一样的;
- java中的hash表有个特别的地方,Integer这一类,在hash表内部也都会理解为基本类型存入表内,如果是非原生类型,则按引用类型;
/**
例如如下例子:
*/
public static class Zuo{
private int value;
public Zuo(int a){
this.value = a;
}
}
public static void main(String[] args){
HashMap<Integer,String> test = new HashMap<>();
Integer a = 190000;
Integer b = 190000;
// 下面代码会返回false
System.out.println(a == b);
test.put(a,"我是3");
// 下面代码回返回true
System.out.println(test.contrailKey(b));
Zuo zuo1 = new Zuo(1);
Zuo zuo2 = new Zuo(2);
test.put(zuo1,"我是z1");
// 下面代码会返回false
System.out.println(test.contrailKey(zuo2));
}
有序表
- java中TreeMap就是有序表
- TreeMap 是一个接口名
- 红黑树、avl树、sb树(size balance tree)、跳表都可以实现有序表
- 增删改查的时间复杂度都是O(logN),功能比hash表强大,但是时间复杂度比hash表弱
- java中的TreeMap就是红黑树实现的
- 有序表中的key是按序组织的,所以拥有firstKey(最小的key),lastKey(最大的key),floor(离某个值最近的key,<=value),ceilingKey(离某个值最近的key,>=value);
- 不是基础类型需要实现比较器,用于两者间的比较,否则会报错