《算法》1.3~5算法进阶之路-基础

一、基础

1.算术表达式求值

表达式由括号、运算符和操作数(数字)组成。简单起见,这里定义的是未省略括号的算术表达式。我们根据以下4种情况从左到右逐个将这些实体送入栈处理。

  • 将操作数压如操作数栈;
  • 将运算符压入运算符栈;
  • 忽略左括号;
  • 在遇到右括号时,弹出一个运算符,弹出所需数量的操作数,冰江运算符和操作数的运算结果压入操作数栈
  • 本题用到的算数表达式: (1+((2+3)*(4*5)))

示例代码:

import java.util.Stack;

public class test {

    public static void main(String[] args) {
        
        Stack ops = new Stack();   //操作符
        Stack vals = new Stack();   //操作数
        
        String str = "(1+((2+3)*(4*5)))";
        
        for(int i=0; i

2.数组实现一个泛型栈(下压(LIFO)栈)

Item是一个类型参数,用于表示用例将会使用的某种具体类型的象征性的占位符。可以将FixedCapacityStack理解为某种元素的栈,这正是我们想要的结果:依据这个栈可以处理任意数据类型。

示例代码:

import java.util.Iterator;

public class FixedCapacityStack implements Iterable{
    
    private Item[] a;   // Stack entries
    private int N;      // size
    
    public FixedCapacityStack(int cap) {
        a = (Item[]) new Object[cap];
    }

    public boolean isEmpty() { return N==0; }
    
    public int size() { return N; }
    
    public void push(Item item) {
        // 将元素压入站顶
        if(N == a.length) resize(2 * a.length);
        a[N++] = item;
    }
    
    public Item pop() {
        // 从栈顶删除元素
        Item item = a[--N];
        a[N] = null; // 避免对象游离
        if(N>0 && N==a.length/4) resize(a.length/2);
        return item;
    }
    
    public void resize(int max) {
        // 将大小为N <=max 的栈移动到一个新的大小为max的数组中
        Item[] temp = (Item[]) new Object[max];
        for(int i=0; i增加迭代功能
    public Iterator iterator() {
        return new ReverseArrayIterator();
    }
    
    // 定义一个迭代器
    private class ReverseArrayIterator implements Iterator {
        private int i = N;
        public boolean hasNext() { return i > 0;  }
        public Item next()       { return a[--i]; }
        public void remove()     {                }
    }

}
// 何为对象游离? 读者自行百度

3.使用链表实现泛型栈

  • Item是一个类型参数,用于表示用例将会使用的某种具体类型的象征性的占位符。可以将MyStack理解为某种元素的栈,这正是我们想要的结果:依据这个栈可以处理任意数据类型。
    使用链表实现栈的优点:
  • 它可以处理任意类型的数据;
  • 所需的空间总是和集合的大小成正比;
  • 操作所需的时间总是和集合的大小无关;

示例代码:

import java.util.Iterator;

public class MyStack implements Iterable {

    private Node first;   //栈顶(最近添加的元素)
    private int N;        //元素数量
    private class Node {
        // 定义了节点的嵌套类
        Item item;
        Node next;
    }
    public boolean isEmpty() { return first == null; }
    public int size() { return N; }
    public void push(Item item) {
        //向栈顶添加元素
        Node oldfirst = first;
        first = new Node();
        first.item = item;
        first.next = oldfirst;
        N++;
    }
    public Item pop() {
        //从栈顶删除元素并返回元素的值
        Item item = first.item;
        first = first.next;
        N--;
        return item;
    }
    public Iterator iterator() {
        return new ListIterator();
    }
    
    private class ListIterator implements Iterator {
        private Node current = first;
        public boolean hasNext() {
            return current != null;
        }
        public void remove() { }
        public Item next() {
            Item item = current.item;
            current = current.next;
            return item;
        }
        
    }
    
}

4.队列的实现(链表)

  • 要将一个元素入列(enqueue()),我们就将他添加到表尾(但是在链表为空时需要将firstlast都指向新结点);要将一个元素出列(dequeue()),我们就删除表头的结点(代码和Stack的pop()方法相同,只是当链表为空时需要更新last的值)。

示例代码:

import java.util.Iterator;

public class MyQueue implements Iterable {
    
    private Node first;   //指向最早添加的结点的链接
    private Node last;    //指向最近添加的结点的链接
    private int N;        //队列中的元素数量
    private class Node {
        // 定义了节点的嵌套类
        Item item;
        Node next;
    }
    public boolean isEmpty() { return first == null; }
    public int size() { return N; }
    public void enqueue(Item item) {
        //向表尾添加元素
        Node oldlast = last;
        last = new Node();
        last.item = item;
        last.next = null;
        if(isEmpty()) first = last;
        else oldlast.next = null;
        N++;
    }
    public Item dequeue() {
        //从表头删除元素
        Item item = first.item;
        first = first.next;
        if (isEmpty()) last = null;
        N--;
        return item;
    }
    /*iterator()的实现方法不在重复写入*/
    /*测试用例main()的实现在此不再獒述*/
}

5.反转链表

目的:定义一个函数,输入一个链表的头结点,反转该链表并输出反转后链表的头结点0

  • 非递归实现: 为了完成这个任务,需要记录链表中三个连续的节点:reverse、first和second。在每一轮迭代中,我们从原链表中提取结点first并将它插入到逆链表的开头。在这过程中,我们需要一直保持first指向原链表中所有剩余的首结点,second指向原链表中剩余结点的第二个节点,reverse指向结果链表中的首结点。
  • 递归实现: 假设链表有N个结点,我们先递归颠倒最后N-1个结点,然后小心地将原链表中的首结点插入到结果链表地末端。

示例代码:

// 非递归解法
public Node reverse(Node x) {
    Node first = x;
    Node reverse = null;
    while(first != null) {
        Node second = first.next;
        first.next = reverse;
        reverse = first;
        first = second;    
    }
}

// 递归解法
public Node reverse(Node first) {
    if(first == null) return null;
    if(first.next == null) return first;
    Node second = first.next;
    Node rest = reverse(second);
    second.next = first;
    first.next = null;
    return rest;
}

6.union-find算法

  • 描述: 动态连通性问题。输入一列整数对,其中每一个整数都表示一个某种类型的对象(这里用的是int型的值作为触点),一对整数p q可以被理解为“p和q是相连的”,我们假设“相连”是一种等价关系。这意味着它们具有自反性、对称性、传递性
  • 等价关系能将对象分为多个等价类。在这里,当且仅当两个对象相连时他们才属于同一个等价类。我们的目标是编写一个程序来过滤掉序列中所有无意义的整数对(两个整数均来自于同一个等价类中)。当程序输入整数对p q时,如果所有的整数对都不能说明p和q是相连的,那么程序应该忽略p q这对整数并继续处理输入中的下一对整数。
  • 术语: 在这里:对象将被称为触点、整数对称将被称为连接、等价类将被称为连通分量或是分量
  • 读者自行百度一下问题详细描述,再次不做详述。

准备工作:

我们需要维护两个实例变量,一个是连通分量的个数,另一个是数组id[]find()union()的实现将会是后面讨论的主题,也是union-fion算法的重点。
说明:我们可以用一个以触点为索引的数组id[]作为基本的数据结构来表示所有的分量。我们将使用分量中的某个触点的名称作为分量的标识符,因此你可以认为每个分量都是由它的触点之一所标识的。一开始,我们有N个分量,每个触点都构成了一个只含有他自己的分量。因此我们将id[i]的值初始为i。对于每一个触点i,我们将find()方法用来判定它所在的分量所需的信息保存在id[i]之中。connected()方法的实现只用一条语句find(p) == find(q),它返回一个布尔值。

public class UF {

    private int[] id;   //分量id(以触点作为索引)
    private int count;  //分量数量
    
    public UF(int N) {
        //初始化分量id数组
        count = N;
        id = new int[N];
        for(int i=0; i

1.quick-find算法

思路:

  • 这种方法是保证当且仅当id[p]等于id[q]时p和q是连通的,也就是在同一连通分量中的所有触点i对应的id[i]中的值必须全部相同。这意味着只需要调用connected()判断p和q是否在同一分量中,如果是,则不进行任何操作。如果不是,则使用union()进行p所在分量与q所在分量的连接操作。此处,我们可以便遍历整个数组,将所有和id[p]相等的元素的值变为id[q]的值。
  • quick-find算法的性能并不是最好的,相反,该算法只适用于解决小数量的问题,如果用于解决大型问题,那么所需要的时间是不可估量的。

示例代码:

    public int find(int p){ 
        return id[p]; 
    };

    public void union(int p, int q) {
        //将p和q归并到相同的分量中
        int pID = find(p);
        int qID = find(q);
        
        //如果已经在相同的分量之中则不需要采取任何行动
        if(pID == qID) return;
        
        //将p的分量重命名为q的名称
        for(int i=0; i

2.quick-union算法

思路:

  • 正如算法名称一样,quick-union算法主要用于提高union()方法的速度,他和quick-find算法是互补的。
  • 以上两种算法具有相同的数据结构,但我们赋予这些值的意义不同,我们需要用它们来定义更加复杂的数据结构。每个触点所对应的id[]元素都是同一分量中的另一个触点的名称(也可能是它自己)——我们将这种联系称为链接
  • 我们从给定的触点开始,由它的链接得到另一个触点,再由这个触点的链接到达第三个触点,如此继续跟随着链接直到到达一个跟触点,即链接指向自己的触点(你会发现,这样一个触点必然存在)。
  • 为何这样一个跟触点必然存在?:每次union()方法链接两个分量时都会事先到达跟触点,然后修改跟触点中对应的链接,这样使得每个触点都会有唯一的跟触点。如果读者不能理解,不妨拿出一张白纸,模拟一下查找与连接函数执行的过程,你会发现上面的理论是正确的。

示例代码:

    public int find(int p){ 
        //找出分量的名称
        while(p != id[p]) p = id[p];
        return p;
    };
    public void union(int p, int q) {
        //将p和q的根节点统一
        int pRoot = find(p);
        int qRoot = find(q);
        
        //如果已经在相同的分量之中则不需要采取任何行动
        if(pRoot == qRoot) return;
        
        id[pRoot] = qRoot;
        
        count--;
    }

3.带权quick-union算法

思路:

  • quick-union算法已经对quick-find算法进行了相当不错的改进,但是它还是由一个很大的缺点。
  • 在这里,分量之间的合并很类似于森林合并成树的操作。我们将分量称为一棵树,分量的长度称为树的深度,触点称为节点。现在我们来想一下,假如每次进行p和q的根节点统一操作时,都是深度较大的向深度较小的分量靠拢,那么最终将会形成一个深度很大的分量,而后每次进行find()方法时,可能需要很长的时间,这是随着程序运行而必然发生的情况,既然我们无法避免问题,那我们就要想办法去解决问题。。
  • 我们只需要简单地修改quick-union算法就能保证这样的糟糕情况不再出现。我们会记录每一棵树的大小并总是将较小的树连接到较大的树上。这项改动需要添加一个用于记录树中节点数的数组。

示例代码:

public class WeightedQuickUnionUF {

    private int[] id;   //父链接数组(以触点作为索引)
    private int[] sz;   //(由触点索引的)各个根节点所对应的分量的大小
    private int count;  //连通分量数量
    
    public WeightedQuickUnionUF(int N) {
        //初始化分量id数组
        count = N;
        id = new int[N];
        for(int i=0; i

7.测量程序运行所需的时间

  • Stopwatch类elapsedTime()方法 能够返回自它被创建以来所经历的时间长度,以秒为单位。它的实现基于Java系统的currentTimeMillis()方法,该方法能够返回以毫秒计数的当前时间。它在被构造时纪录了当前时间,并在elapsedTime()方法被调用时再次调用该方法来计算得到对象创建以来所经过的时间。
public class Stopwatch {
    private final long start;
    public Stopwatch() {
        start = System.currentTimeMillis();
    }
    public double elapsedTime() {
        long now = System.currentTimeMillis();
        return (now - start) / 1000.0;
    }
}

声明:发表此文是出于传递更多信息之目的,并且做一些学习笔录是为了日后学习之用。文章大部分代码示例与文字内容均摘自《算法》一书。若有来源标注错误或侵犯了您的合法权益,请作者持权属证明与本我们(QQ:981086665;邮箱:[email protected])联系联系,我们将及时更正、删除,谢谢。

你可能感兴趣的:(《算法》1.3~5算法进阶之路-基础)