赞
踩
数据结构和算法作为程序员的基本功,一定得稳扎稳打的学习,我们常见的框架底层就是各类数据结构,例如跳表之于redis、B+树之于mysql、倒排索引之于ES,熟悉了底层数据结构,对框架有了更深层次的理解,在后续程序设计过程中就更能得心应手。掌握常见数据结构和算法的重要性显而易见,本文主要讲解了几种常见的数据结构及基础的排序和查找算法,最后对高频算法笔试面试题做了总结。本文是数据结构第三讲:栈/队列
递归的本质 栈
1、递归是函数里调用自身
2、必须有一个明确的递归出口
3、在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储,因此递归次数过多容易造成栈溢出
递归的基本思想:
1、是把规模较大的一个问题,分解成规模较小的多个子问题去解决
2、先解决子问题,再基于子问题来解决当前问题
递归和内存:每次递归调用都在内存中生成一个新的函数副本(仅仅是一些相关的变量),一旦函数结束(即返回某些数据),改返回函数的副本就从内存中删除。
递归一般用于解决三类问题:
1、数据的定义是按递归定义的。(Fibonacci函数,n的阶乘)
2、问题解法按递归实现。(动态规划/分治/回溯)归并排序和快速排序用到了递归的思想
3、数据的结构形式是按递归定义的。(二叉树的/先/中/后序遍历,图的深度/广度优先搜索)
1、栈在表达式求值中的应用:(一个保存操作符的栈,另一个是保存运算符的栈)
我们从左向右遍历表达式,当遇到数字,我们就直接压入操作数栈;当遇到运算符,就与运算符栈的栈顶元素进行比较(栈顶元素优先级高就取出运算符,从操作数栈取两个操作数,结果压入操作数栈)
public int evalRPN(String[] tokens) { Stack<Integer> stack = new Stack<>(); for (int i = 0; i < tokens.length; i++) { String str = tokens[i]; if (str.length() == 1) { char ch = str.charAt(0); if (ch - '0' >= 0 && ch - '0' <= 9) { Integer a = Integer.valueOf(str); stack.push(a); } else {//如果是运算符 if (stack.size() < 2) return 0; int num2 = stack.pop(); int num1 = stack.pop(); switch (ch) { case '+': stack.push(num1 + num2); break; case '-': stack.push(num1 - num2); break; case '*': stack.push(num1 * num2); break; case '/': stack.push(num1 / num2); break; } } } else { int n = Integer.valueOf(str); stack.push(n); } } return stack.pop(); }
栈在括号匹配中的应用:(我们用栈来保存未匹配的左括号,从左到右依次扫描字符串。当扫描到左括号时,则将其压入栈中;当扫描到右括号时,从栈顶取出一个左括号)
public class 有效的括号 { public boolean isValid(String s) { Stack<Character> stack = new Stack<>(); char[] chars = s.toCharArray(); for (char aChar : chars) { if (stack.size() == 0) { stack.push(aChar); } else if (isSym(stack.peek(), aChar)) { stack.pop(); } else { stack.push(aChar); } } return stack.size() == 0; } //括号是否能匹配成功 private boolean isSym(char c1, char c2) { return (c1 == '(' && c2 == ')') || (c1 == '[' && c2 == ']') || (c1 == '{' && c2 == '}'); } }
变体1:给定一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长的包含有效括号的子串的长度 例如:输入: "(()"输出: 2 输入: “)()())” 输出: 4
对于这种括号匹配问题,一般都是使用栈,我们先找到所有可以匹配的索引号,然后找出最长连续数列!O(nlogn)
public class 最长有效括号 { public int longestValidParentheses(String s) { if (s == null || s.length() == 0) return 0; Deque<Integer> stack = new ArrayDeque<>(); stack.push(-1); //System.out.println(stack); int res = 0; for (int i = 0; i < s.length(); i++) { if (s.charAt(i) == '(') stack.push(i); else { stack.pop(); if (stack.isEmpty()) stack.push(i); else { res = Math.max(res, i - stack.peek()); } } } return res; } }
思路2:动态规划
public int longestValidParentheses(String s) { if (s == null || s.length() == 0) return 0; int[] dp = new int[s.length()];//状态转移表 下标表示对应考察元素 返回值表示最长有效括弧 int res = 0; for (int i = 0; i < s.length(); i++) { if (i > 0 && s.charAt(i) == ')') { if (s.charAt(i - 1) == '(') { dp[i] = (i - 2 >= 0 ? dp[i - 2] + 2 : 2); } else if (s.charAt(i - 1) == ')' && i - dp[i - 1] - 1 >= 0 && s.charAt(i - dp[i - 1] - 1) == '(') { dp[i] = dp[i - 1] + 2 + (i - dp[i - 1] - 2 >= 0 ? dp[i - dp[i - 1] - 2] : 0); } } res = Math.max(res, dp[i]); } return res; }
编程题5:如何实现浏览器的前进、后退功能?
我们使用两个栈,X和Y,我们把首次浏览的页面依次压入栈X,当点击后退按钮时,再依次从栈X中出栈,并将出栈的数据依次放入栈Y.当我们点击前进按钮时,
我们依次从栈Y中取出数据,放入栈X中。当栈X中没有数据时,那就说明没有页面可以继续后退浏览了。当栈Y中没有数据,那就说明没有页面可以点击前进按钮浏览了。
递归需要满足的三个条件:1、一个问题的解可以分解为几个问题的解;2、这个问题与分解后的子问题,除了数据规模不同,求解思路完全一样;3、存在递归终止条件
首先是定义ListNode,最基础的数据结构(包含int value,指向下一个结点点的指针),然后有结点构成栈(包含pop/push/print/clear等功能),最后实现浏览器功能()
public class 用栈实现浏览器的前进后退 { private String currentPage; //使用两个栈,X和Y private LinkedListBasedStack backStack; //LinkedListBasedStack为基于链表实现的栈,功能有入栈/出栈/获取栈顶元素/打印栈中元素 private LinkedListBasedStack forwardStack; //构造函数 public 用栈实现浏览器的前进后退() { this.backStack = new LinkedListBasedStack();//第一个栈 打开新页面时入栈,页面前进时入栈 后退时出栈 this.forwardStack = new LinkedListBasedStack();//第二个栈 前进时出栈 后退时入栈 } public void open(String url) { if (this.currentPage != null) { this.backStack.push(this.currentPage);//入栈 第一个栈 this.forwardStack.clear(); } showUrl(url, "Open"); } public boolean canGoBack() { return this.backStack.size() > 0; } public boolean canGoForward() { return this.forwardStack.size() > 0; } //后退功能 public String goBack() { if (this.canGoBack()) { this.forwardStack.push(this.currentPage);//第二个栈入栈 String backUrl = this.backStack.pop();//第一个栈出栈 showUrl(backUrl, "Back"); return backUrl; } System.out.println("* Cannot go back, no pages behind."); return null; } //前进功能 public String goForward() { if (this.canGoForward()) { this.backStack.push(this.currentPage);//第一个栈入栈 String forwardUrl = this.forwardStack.pop();//第二个栈出栈 showUrl(forwardUrl, "Foward"); return forwardUrl; } System.out.println("** Cannot go forward, no pages ahead."); return null; } public void showUrl(String url, String prefix) { this.currentPage = url; System.out.println(prefix + " page == " + url); } public void checkCurrentPage() { System.out.println("Current page is: " + this.currentPage); } }
编程题3:用数组实现一个顺序栈
public class 用数组实现栈 { private String[] items; // 数组 private int count; // 栈中元素个数 private int n; // 栈的大小 // 初始化数组,申请一个大小为n的数组空间 public 用数组实现栈(int n) { this.items = new String[n]; this.n = n; this.count = 0; } // 入栈操作 public boolean push(String item) { // 数组空间不够了,直接返回false,入栈失败。 if (count == n) return false; // 将item放到下标为count的位置,并且count加一 items[count] = item; ++count; return true; } // 出栈操作 public String pop() { // 栈为空,则直接返回null if (count == 0) return null; // 返回下标为count-1的数组元素,并且栈中元素个数count减一 String tmp = items[count - 1]; --count; return tmp; } }
编程题4:用链表实现一个链式栈
public class 用链表实现栈 { private ListNode top = null; //入栈 public void push(int value) { ListNode newNode = new ListNode(value, null); //判断是否栈空 if (top == null) { top = newNode; } else { newNode.next = top; top = newNode; } } //出栈 public int pop() { if (top == null) return -1; int value = top.data; top = top.next; return value; } public void printAll() { ListNode p = top; while (p != null) { System.out.print(p.data + " "); p = p.next; } System.out.println(); } }
java concurrent并发包利用ArrayBlockingQueue来实现公平锁等)
分类:顺序队列和链式队列(用数组实现的队列和链表实现的队列) 基于链表实现的无界队列(可能会导致过多的请求排队,响应时间较长),基于数组实现的有界队列(大小有限)
Disruptor(线程之间用于消息传递的队列)(应用apache Storm/canal/log4j2) 性能比常用的内存消息队列ArrayblockingQueue要高出一个数量级,它还因此获得过Oracle官方的Duke大奖
1、Disruptor详解?
基于循环队列保证数据被消费的顺序性。(实现了一个最简单的“生产者-消费者模型”)在这个模型中,“生产者”生产数据,并且将数据放到一个中心存储容器中。之后,“消费者”从中心存储容器中,取出数据消费。而存储数据的中心存储容器,是用什么样的数据结构来实现的呢?(1、基于链表实现的链式队列;2、基于数组实现的顺序队列(循环队列))
基于循环队列的生产者/消费者模型:思路(当队列满了之后,生产者就轮询等待,当队列空了后,消费者就轮训等待)
public class Queue { private Long[] data;//基于数据实现 private int size = 0, head = 0, tail = 0; public Queue(int size) { this.data = new Long[size]; this.size = size; } public boolean add(Long element) { if ((tail + 1) % size == head) return false;//循环队列满了 data[tail] = element; tail = (tail + 1) % size; return true; } public Long poll() { if (head == tail) return null;//循环队列为空 long ret = data[head]; head = (head + 1) % size; return ret; } } public class Producer { private Queue queue; public Producer(Queue queue) { this.queue = queue; } public void produce(Long data) throws InterruptedException { while (!queue.add(data)) { Thread.sleep(100);//说明添加失败,队列为满,等待消费 } } } public class Consumer { private Queue queue; public Consumer(Queue queue) { this.queue = queue; } public void comsume() throws InterruptedException { while (true) { Long data = queue.poll(); if (data == null) { Thread.sleep(100); } else { // TODO:...消费数据的业务逻辑... } } } }
上述代码存在的问题:多线程下,多个生产者写入的数据可能会互相覆盖,多个消费者可能会读取重复的数据
解决方法:1、加锁(同一时间只允许一个线程执行add()函数,相当于并行改成了串行),可以使用CAS乐观锁机制减少加锁的粒度。
基于无锁的并发“生产者-消费者模型”
编程题1:用数组实现一个顺序队列
public class 用数组实现的队列 { // 数组:items,数组大小:n private String[] items; private int n = 0; // head表示队头下标,tail表示队尾下标 private int head = 0; private int tail = 0; // 申请一个大小为capacity的数组 public 用数组实现的队列(int capacity) { items = new String[capacity]; n = capacity; } // 入队 public boolean enqueue1(String item) { // 如果tail == n 表示队列已经满了 if (tail == n) return false; items[tail] = item; ++tail; return true; } //入队操作,将item放入队尾 并更新head/tail的索引 可以动态扩容的队列 public boolean enqueue2(String item) { // tail == n表示队列末尾没有空间了 if (tail == n) { //tail ==n && head==0,表示整个队列都占满了 if (head == 0) return false;// 表示整个队列都占满了 // 数据搬移 for (int i = head; i < tail; ++i) { items[i - head] = items[i]; } //搬移完之后重新更新head和tail tail -= head; head = 0; } items[tail] = item; ++tail; return true; } // 出队 public String dequeue() { // 如果head == tail 表示队列为空 if (head == tail) return null; // 为了让其他语言的同学看的更加明确,把--操作放到单独一行来写了 String ret = items[head]; ++head; return ret; } }
编程题2:用链表实现一个链式队列
public class 基于链表实现的队列 { // 队列的队首和队尾 private ListNode head = null; private ListNode tail = null; // 入队 public void enqueue(String value) { if (tail == null) { //新建的队列 ListNode newNode = new ListNode(value, null); head = newNode; tail = newNode; } else { tail.next = new ListNode(value, null); tail = tail.next; } } // 出队 public String dequeue() { if (head == null) return null; String value = head.data; head = head.next; if (head == null) { tail = null; } return value; } public void printAll() { ListNode p = head; while (p != null) { System.out.print(p.data + " "); p = p.next; } System.out.println(); } }
编程题3:实现一个循环队列(最关键的是,确定好队空和队满的判定条件)(我使用数组实现)
队列为空的判断条件仍然是head == tail,当队满时,(tail+1)%n=head,循环队列会浪费一个数组的存储空间。
public class 循环队列 { //数组:items,数组大小:n private String[] items; private int n = 0; // head表示队头下标,tail表示队尾下标 private int head = 0; private int tail = 0; // 申请一个大小为capacity的数组 public 循环队列(int capacity) { items = new String[capacity]; n = capacity; } // 入队 public boolean enqueue(String item) { // 队列满了 if ((tail + 1) % n == head) return false; items[tail] = item; tail = (tail + 1) % n; return true; } // 出队 public String dequeue(){ // 如果head == tail 表示队列为空 if (head == tail) return null; String ret = items[head]; head = (head + 1) % n; return ret; } }
编程题4:实现一个双端队列 java中有工具包Deque
public class 自己动手实现双端队列 { private Object[] data; private int head = 0; private int tail = 0; public 自己动手实现双端队列(int k) { data = new Object[k]; } public boolean insertFront(int value) { if(isFull()){ return false; } head= decr(head); data[head] = value; return true; } public boolean insertLast(int value) { if(isFull()){ return false; } data[tail] = value; tail = incr(tail); return true; } public boolean deleteFront() { if(isEmpty()){ return false; } data[head] = null; head = incr(head); return true; } public boolean deleteLast() { if(isEmpty()){ return false; } tail = decr(tail); data[tail] = null; return true; } public int getFront() { if(isEmpty()){ return -1; } return (int)data[head]; } public int getRear() { if(isEmpty()){ return -1; } return (int) data[decr(tail)]; } public boolean isEmpty() { return head == tail && data[head] == null && data[tail] == null; } public boolean isFull() { return tail== head && data[head] != null && data[tail] != null; } //前进一步 private int incr(int index){ return ++index % data.length; } //后退一步 private int decr(int index){ return (--index + data.length) % data.length; } }
编程题5:滑动窗口最大值
public int[] maxSlidingWindow(int[] nums, int k){ if(nums==null||nums.length<2) return nums; //双向队列 保存当前窗口最大值的数组位置 保证队列中数组位置的数按从大到小排序 LinkedList<Integer> list = new LinkedList(); // 结果数组 int[] result = new int[nums.length-k+1]; for(int i=0;i<nums.length;i++){ //保证从大到小 如果前面数小 弹出 while(!list.isEmpty()&&nums[list.peekLast()]<=nums[i]){ list.pollLast(); } //添加当前值对应的数组下标 list.addLast(i); //初始化窗口 等到窗口长度为k时 下次移动在删除过期数值 if(list.peek()<=i-k){ list.poll(); } //窗口长度为k时 再保存当前窗口中最大值 if(i-k+1>=0){ result[i-k+1] = nums[list.peek()]; } } return result; }
public class 两个栈实现队列<E> {
Stack<E> s1 = new Stack<E>();//E为链表或数组
Stack<E> s2 = new Stack<E>();
public synchronized void put(E e) {
s1.push(e);
}
public synchronized E pop() {
if (s2.isEmpty()) {
while (!s1.isEmpty()) {
s2.push(s1.pop());
}
}
return s2.pop();
}
}
编程题7:使用两个队列实现栈
思路:确保有一个队列总是空的, 入栈:在任何一个非空队列中插入元素,检查队列q1是否为空,如果q1为空,那么对q2执行入队操作;
出栈:如果队列q1非空,那么从q1移n-1个元素到q2中,然后对q1中的最后一个元素执行出队操作并返回该元素。
public class 使用队列实现栈<E> { Queue<E> queue1 = new LinkedBlockingQueue<E>();//E为链表或数组; Queue<E> queue2 = new LinkedBlockingQueue<E>();; public void push(E data) { if (queue1.isEmpty()) { queue2.add(data); } else { queue1.add(data); } } public E Pop(){ int i,size; if (queue2.isEmpty()) { size = queue1.size(); i=0; while (i < size-1) { queue2.add(queue1.remove()); i++; } return queue1.remove(); }else { size = queue2.size(); i=0; while (i < size-1) { queue1.add(queue2.remove()); i++; } return queue2.remove(); } } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。