关于《算法》第一章的一点总结

第一章 基础

1.1 基础编程模型

1.1.1 Java程序的基本结构

  • 原始数据类型:整型(int),浮点型(double),布尔型(boolean),字符型(char)
  • 语句:声明,赋值,条件,循环,调用,返回。
  • 数组
  • 静态方法:能够封装并重用代码,使咱们能够用独立的模块开发程序。
  • 字符串
  • 标准输入/输出
  • 数据抽象:数据抽象封装和重用代码,使咱们能够定义非原始数据类型,进而支持面向对象编程。

clipboard.png

1.1.2 原始数据类型与表达式

  • 整型(int)
  • 双精度实数类型(double)
  • 布尔型(boolean)
  • 字符型(char)

clipboard.png

  • 表达式
  • 类型转换
  • 比较
  • 其余原始类型:java

    • 64位整数long
    • 16位整数short
    • 16位字符char
    • 8位整数byte
    • 32位单精度实数float

1.1.3 语句

  • 声明语句
  • 赋值语句
  • 条件语句
  • 循环语句
  • 调用和返回语句

1.1.4 简便记法

  • 声明并初始化node

    • 变量在声明时初始化
  • 隐式赋值算法

    • i++
    • --i
    • i/=2
    • i+=2
    • ...
  • 单语句代码段编程

    • 条件或循环语句代码段只有一句语句,花括号能够省略
  • for语句

clipboard.png

1.1.5 数组

  • 建立数组

    • 声明数组的名字和类型
    • 建立数组
    • 初始化数组元素
  • 起别名网络

    • 若是将一个数组变量赋予另外一个变量,那么两个变量将会指向同一个数组
      int[] a =new int[N];
      ...
      a[i] = 1234;
      ...
      int[] b = a;
      ...
      b[i] = 5678//a[i]的值也会编程5678

1.1.6 静态方法

clipboard.png

  • 方法的性质数据结构

    • 方法的参数是按值传递:在方法中参数变量的使用方法和局部变量相同,惟一不一样是参数变量的初始值由调用方提供。方法处理的是参数的值,而非参数自己。在静态方法中改变一个参数变量的值对调用者无影响。
    • 方法名能够被重载:例如,Java的Math包使用这种方法给全部的原始数值类型实现了Math.abs()、Math.min()、Math.max()。另外一种用法是为函数定义两个版本,一个须要一个参数另外一个则为该参数提供一个默认值。
    • 方法只能返回一个值,但能够包含多个返回语句:尽管可能存在多条返回语句,任何静态方法每次都只会返回一个值,即被执行的第一个条返回语句的参数。
    • 方法能够产生反作用:void类型的静态方法会产生反作用(接受输入、产生输出、修改数组或改变系统状态)
  • 递归模块化

    • 编写递归代码时最重要的有如下三点:函数

      • 递归总有一个最简单的状况——方法的第一条语句老是一个包含return的条件语句。
      • 递归调用老是去尝试解决一个规模更小的子问题。
      • 递归调用的父问题和尝试解决的子问题之间不该该有交集。工具

        二分查找的递归实现
        public static int rank(int key, int[] a)
        {   return rank(key, a, 0, a.length - 1); }
        public static int rank(int key, int[] a, int lo, int hi)
        {   //若是key存在于a[]中,它的索引不会小于lo且不会大于hi
        
            if(lo > hi) return -1;
            int mid = lo + (hi - lo) / 2;
            if(key < a[mid]) return rank(key, a, lo, mid -1 );
            else if(key > a[mid]) return rank(key, a, mid + 1, hi);
            else return mid;
        }
  • 基础编程模型

    • 静态方法库是定义在一个Java类中的一组静态方法
    • Java开发的基本模式是编写一个静态方法库来完成一个任务
  • 模块化编程

    • 程序总体的代码量很大时,每次处理的模块大小仍然适中
    • 能够共享和重用代码而无需从新实现
    • 很容易用改进的实现替换老的实现
    • 能够为解决编程问题创建合适的抽象模型
    • 缩小调试范围
  • API

    • 应用程序接口

      • 提供方法须要知道的全部信息

1.1.8 字符串

  • 自动转换:Java在链接字符串的时候会自动将任意数据类型的值转换为字符串:若是加号(+)的一个参数是字符串,那个Java会自动将其余参数都转换为字符串。这样,经过一个空字符串“ ”可将任意数据类型的值转换为字符串值。

1.1.9 输入输出

  • 重定向与管道

    • 重定向:只须要向启动程序的命令中加入一个简单的提示符,就能够将它的标准输出重定向至一个文件
    • 管道:将一个程序的输出重定向为另外一个程序的输入

1.1.10 二分查找

import java.util.Arrays;
    public class BinarySearch
    {
        public static int rank(int key, int[] a)
        {    //数组必须是有序的
            int lo = 0;
            int hi = a.length - 1;
            while(lo <= hi)
            {    //被查找的键要么不存在,要么必然存在与a[lo..hi]之中
                int mid = lo + (hi - lo) / 2;
                if (key < a[mid]) hi = mid -1;
                else if (key > a[mid]) lo = mid + 1;
                else return mid;
            }
            return -1;
        }
        public static void main(String[] args)
        {
            int[] whitelist = In.readInts(args[0]);
            Arrays.sort(whitelist);
            while(!StdIn.isEmpty())
            {    //读取键值,若是不存在于白名单中则将其打印
                int key = StdIn.readInt();
                if(rank(key.whitelist) < 0)
                    StdOut.println(key);
            }
        }
    }

1.2 数据抽象

1.2.1 使用抽象数据类型

  • 抽象数据类型(ADT)的定义和静态方法库共同之处:

    • 二者的实现均为Java类
    • 实例方法可能接受0个或多个指定类型的参数,由括号表示而且逗号分隔;
    • 它们可能返回一个指定类型的值,也能不会(用void表示)。
  • 不一样:

    • API中可能会出现若干个名称和类型相同且没有返回值的函数。称为构造函数
    • 实例方法不须要static关键字。它们不是静态方法——它们的目的就是操做该数据类型中的值
    • 某些实例方法的存在是为了尊重Java的习惯,此类方法称为继承的方法并在API中将它们显示为灰色。

1.2.3 抽象数据类型的实现

clipboard.png

  • 实例变量:和静态方法或局部变量最关键的区别:每一个时刻局部变量只会有一个值,而每一个实例变量可对应着无数值(数据类型的每一个实例对象都会有一个)。在访问实例变量时都须要经过一个对象——咱们访问的是这个对象的值。每一个实例变量的声明都须要一个可见性修饰符(private:对本类可见)
  • 构造函数:每一个java类至少含有一个构造函数以建立一个对象的标识。 用于初始化实例变量,它能偶直接访问实例变量且没有返回值。若是没有定义构造函数,类将会隐式定义一个默认状况下不接受任何参数的构造函数并将全部实例变量初始化为默认值。
  • 实例方法:每一个实例方法都有一个返回值类型、一个签名(它指定了方法名、返回值类型和全部参数变量的名称)和一个主体(它有一系列语句组成,包含一个返回语句来说一个返回类型的值传递给调用者)。与静态方法关键不一样:它们能够访问并操做实例变量。
  • 能够经过触发一个实例方法来操做该对象的值。
  • 做用域:

    • 参数变量:整个方法
    • 局部变量:当前代码段中它的定义以后的全部语句
    • 实例变量:整个类

1.2.4 更多数据类型的实现

  • 日期
  • 维护多个实现
  • 累加器
  • 可视化累加器

1.2.5 数据类型的设计

  • 封装

    • 容许

      • 独立开发用例和实现的代码
      • 切换至改进的实现而不会影响用例的代码
      • 支持还没有编写的程序(对于后续用例,API可以起到指南的做用)
    • 隔离了数据类型的操做

      • 限制潜在的错误
      • 在实现中添加一致性检查等调试工具
      • 确保用例代码更清晰
  • 接口继承:子类型,容许经过指定一个含有一组公共方法的接口为两个原本没有关系的类创建一种联系,这两个类都不准实现这些方法。

    public interface Datable
    {
      int month();
      int day();
      int year();
    }
    public class Date implements Datable
    {
      //实现代码
    }
  • 实现继承:子类
  • 等价性:

    • java约定equals()必须是一种等价性关系。它必须具备:

      • 自反性,x.equals(x)为true
      • 对称性,当且仅当y.equals(x)为true时,x.equals(y)返回true
      • 传递性,若是x.equals(y)和y.equals(z)均为true,x.equals(z)也将为true
    • 另外,它必须接受一个Object为参数并知足如下性质:

      • 一致性,当两个对象均未被修改时,反复调用x.equals(y)老是会返回相同的值
      • 非空性,x.equals(null)老是返回false
      • 不可变性:final只能用来保证原始数据类型的实例变量的不可变性,而没法用于引用类型的变量。 若是一个应用类型的实例变量含有修饰符final,该实例变量的值(某个对象的引用)永远没法改变——它将永远指向同一个对象,但对象的值自己仍然是可变的。

        public class Vector
        {
          private final double[] coords;
          public Vector(double[] a)
          {
            coords = a;
          }
          ...
        }
        
        用例程序能够经过给定的数组建立一个Vector对象,并在构造对象执行以后改变Vector中的元素的值:
        double[] a = {3.0, 4.0};
        Vector vector = new Vector(a);
        a[0] = 0.0;//绕过 了公有API
  • 异常(Exception),通常用于处理不受咱们控制的不可预见的错误
  • 断言(Assertion),验证咱们在代码中做出的一些假设

1.3 背包、队列和栈

1.3.1 集合型抽象数据类型

clipboard.png
clipboard.png

  • 泛型

    • 集合类的抽象数据类型的一个关键特性:能够用它们存储任意类型的数据,称为泛型或参数化类型。API中,类名后的<Item>记号将Item定义为一个类型参数。它是一个象征性的占位符,表示的是用例将会使用的某种具体数据类型。
例如,编写用栈来处理String对象: 
        java 
        Stack<String> stack = new Stack<String>(); 
        stack.push("Test"); 
        ... 
        String next = stack.pop(); 

使用队列处理Date对象: 
        java 
        Queue<Date> queue = new Queue<Date>(); 
        queue.enqueue(new Date(12, 31, 1999)); 
        ... 
        Date next = queue.dequeue();
  • 自动装箱

    • 类型参数必须被实例化为引用参数。java的封装类型都是原始数据类型对应的引用类型:Boolean、Byte、Character、Double、Float、Integer、Long和Short分别对应着boolean、byte、character、double、float、integer、long和short。在处理赋值语句、方法的参数和算术或逻辑表达式时,java会自动在引用类型和对应的原始数据类型之间进行转换。 即自动装箱与自动拆箱。
java 
        Stack<Integer> stack = new Stack<Integer>(); 
        stack.push(17);//自动装箱(int -> Integer) 
        int i = stack.pop();//自动拆箱(INteger -> int)
  • 可迭代集合类型

    • 迭代访问集合中的全部元素
例如,假设用例在Queue中维护一个交易集合 
java 
        Queue<Transaction> collection = new Queue<Transaction>(); 
        //若是集合是可迭代的,用例用一行语句便可打印出交易的列表: 
        for (Transaction t : collection){ StdOut.print(t);} 
这种语法叫foreach语句
  • 背包

    • 是一种不支持从中删除元素的集合数据类型——它的目的是帮助用例收集元素并迭代遍历全部收集到的元素(用例也能够检查背包是否为空或者获取背包中元素的数量)。迭代的顺序不肯定且与用例无关。

clipboard.png

简单的计算输入中全部double值的平均值和样本标准差。注意:不须要保存全部的数也能够计算标准差。

public ckass Stats
{
  public static void main(String[] args)
  {
    Bag<Double> numbers = new Bag<Double>();
    while(!StdIn.isEmpty())
        numbers.add(StdIn.readDouble());
    int N = numbers.size();
    double sum = 0.0;
    for (double x : numbers)
        sum += x;
    double mean = sum/N;
    sum = 0.0;
    for(double x : numbers)
        sum +=(x - mean)*(x - mean);
    double std = Math.sqrt(sum/(N-1));
    StdOut.printf("Mean: %.2f\n", mean);
    StdOut.printf("Std dev: %.2f\n", std);
  }
}
  • 队列

    • 一种基于先进先出(FIFO)策略的集合类型。用集合保存元素的同时保存它们的相对顺序:是它们入列顺序和出列顺序相同。

clipboard.png

In类的静态方法readInts()的一种实现,该方法解决的问题:用例无需预先知道文件的大小便可将文件中的全部整数读入一个数组中。
public static int[] readInts(String name)
{
    In in = new In(name);
    Queue<Integer> q = new Queue<Integer>();
    while (!in.isEmpty())
        q.enqueue(in.readInt());
    int N = q.size();
    int [] a = new int[N];
    for (int i = 0; i < N; i++)
        a[i] = q.dequeue();
    return a;
}
    • 一种基于后进先出(LIFO)策略的集合类型。

clipboard.png

把标准输入中的全部整数逆序排列,无需预先知道整数的多少。
public class Reverse
{
    public static void main(String[] args)
    {
      Stack<Integer> stack;
      stack = new Stack<Integer>();
      while(!StdIn.isEmpty())
          stack.push(StdIn.readInt());
      for (int i : stack)
          StdOut.println(i);
    }
}
  • Dijikstra的双栈算术表达式求值算法

    • 将操做数要入操做数栈
    • 将运算符压入运算符栈
    • 忽略左括号
    • 在遇到右括号时,弹出一个运算符,弹出所需数量的操做数,并将运算符和操做数的运算结果压入操做数栈。
java 
public class Evaluate 
{ 
    public static void main(String[] args) 
    Stack<String> ops = new Stack<Double>(); 
    while(!StdIn.isEmpty()) 
    {   //读取字符,若是是运算符则压入栈
        String s = StdIn.readString(); 
        if (s.equals("(")); 
        else if (s.equals("+")) ops.push(s); 
        else if (s.equals("-")) ops.push(s); 
        else if (s.equals("*")) ops.push(s); 
        else if (s.equals("/")) ops.push(s); 
        else if (s.equals("sqrt")) ops.push(s); 
        else if (s.equals(")")) 
        {   //若是字符为“)”,弹出运算符和操做数,计算结果并压入栈
            String op = ops.pop(); 
            double v = vals.pop(); 
            if (op.equals("+")) v = vals.pop() + v; 
            else if (op.equals("+")) v = vals.pop() - v; 
            else if (op.equals("+")) v = vals.pop() * v; 
            else if (op.equals("+")) v = vals.pop() / v; 
            else if (op.equals("+")) v = Math.sqrt(v); 
            vals.push(v) 
        }   //若是字符既非运算符也不是括号,将它做为double之压入栈
        else vals.push(Double.parseDouble(s));//字符是数字 
    } 
    StdOut.println(vals.pop()); 
}

1.3.2 集合类数据类型的实现

  • 栈(可以动态调整数组大小的实现)

    • 每项操做的用时与集合大小无关;
    • 空间需求老是不超过集合大小乘以一个常数。
    • 存在缺点:某些push()、pop()操做会调整数组的大小,这项操做的耗时跟栈大小成正比
import java.util.Iterator;
public class ResizingArrayStack<Item> implements Iterable<Item>
{
private Item[] a = (Item[]) new Object[1];//栈元素。java不容许建立泛型数组,所以须要使用类型转换
private int N = 0;//元素数量
public boolean isEmpty() {return N == 0;}
public int size() {return N;}
private void resize(int max)
{//因为java数组建立后没法改变大小,采用建立大小为max的新数组来替代旧数组的方式动态改变数组实际大小
  Item[] temp = (Item[]) new Object[max];
  for (int i = 0;i < N; i++)
      temp[i] = a[i];
  a = temp;
}
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 Iterator<Item> iterator()
{ return new ReverseArrayIterator(); }
private class ReverseArrayIterator implements Iterator<Item>
{//支持后进先出的迭代
  private int i = N;
  public boolean hasNext() { return i > 0;}
  public Item next() { return a[--i];}
  public void remove() { }
}
}

1.3.3 链表

  • 链表是一种递归的数据结构,它或者为空(null),或者是指向一个结点(node)的引用,该结点含有一个泛型的元素和一个指向另外一条链表的引用。
  • 用一个嵌套类来定义节点的抽象数据类型
在须要使用Node类的类中定义它并将它标记为private,由于它不是为用例准备的。
private class Node    
{
    Item item;
    Node next;
}

经过new Node()触发(无参数的)构造函数来建立一个Node类型的对象。调用的结果是一个指向Node对象的引用,它的实例变量均被初始化为null。Item是一个占位符,表示咱们但愿用链表处理的任意数据类型。

  • 构造链表

    • 首先为每一个元素创造一个结点:
    Node first = new Node(); 
    Node second = new Node(); 
    Node thrid = new Node();
    • 将每一个结点的item域设为所需的值(咱们这里假设在这些例子中Item为String):
    first.item = "to"; 
    second.item = "be"; 
    thrid.item = "or";
    • 设置next域来构造链表:
    first.next = second; 
    second.next = third;
    • third.next仍然是null,即对象建立时它被初始化的值。
    • third是一条链表(它是一个结点的引用,该结点指向null,便是一个空链表);

second也是一条链表(它是一个结点的引用,且该结点含有一个指向third的引用,而third是一条链表)
first也是一条链表(它是一个结点的引用,且该结点含有一个指向second的引用,而second是一条链表)

clipboard.png

  • 链表表示的是一列元素。
  • 插入删除元素

    • 在表头插入节点
    • 从表头删除结点(该操做只含有一条赋值语句,所以它的运行时间和链表长度无关)
    • 在表尾插入结点
    • 其余位置的插入和删除操做:使用双向链表,其中每一个结点都好有两个连接,分别指向不一样的方向。

clipboard.png

clipboard.png

clipboard.png

  • 栈的实现

    • 它能够处理任意类型的数据
    • 所需的空间老是和集合的大小成正比
    • 操做所需的时间老是和集合的大小无关

clipboard.png

public class Stack<Item> implements Iterable<Item>
{
    private Node first;//栈顶(最近添加的元素)
    private int N;
    private class Node
    {//定义告终点的嵌套类
      Item item;
      Node next;
    }
    public boolean isEmpty() {return N == 0;}//或: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;
    }
    //iterator()的实现见背包实现算法
    public static void main(String[] args)
    {//输入to be or not to - be - - that - - - is
      Stack<String> s = new Stack<String>();
      while(!StdIn.isEmpty())
      {
        String item = StdIn.readString();
        if(!item.equals("-"))
            s.push(item);
        else if(!s.isEmpty()) StdOut.print(s.pop() + " ");
      }
      StdOut.println("(" + s.size() + " left on stack)");
    }
}
  • 队列的实现

clipboard.png

public class Queue<Item> implements Iterable<Item>
{
private Node first;
private Node last;
private int N;
private class Node
{
  Item item;
  Node next;
}
public boolean isEmpty() {return N == 0;}//或:return first == null;
public int size() {return N;}
public void enqueue(Item item)
{//向表尾添加元素
  Node oldfirst = last;
  last = new Node();
  last.item = item;
  last.next = null;
  if (isEmpty()) first = last;
  else oldfirst.next = last;
  N++;
}
public Item dequeue()
{//从表头删除元素
  Item item = first.item;
  first = first.next;
  if (isEmpty()) last = null;
  N--;
  return item;
}
//
public static void main(String[] args)
{//输入to be or not to - be - - that - - - is
  Queue<String> s = new Queue<String>();
  while(!StdIn.isEmpty())
  {
    String item = StdIn.readString();
    if(!item.equals("-"))
        q.enqueue(item);
    else if(!q.isEmpty()) StdOut.print(q.dequeue() + " ");
  }
  StdOut.println("(" + q.size() + " left on queue)");
}
}
- 背包的实现
```
import java.util.Iterator;
public class Bag<Item> implements Iterable<Item>
{
private Node first;
private class Node
{
  Item item;
  Node next;
}
public void add(Item item)
{
  Node oldfirst = first;
  first = new Node();
  first.item = item;
  first.next = oldfirst;
}
//经过遍历链表使Stack、Queue、Bag变为可迭代的。对于Stack,链表的访问顺序是后进先出;Queue,链表的访问顺序是先进先出;Bag,后进先出顺序,但顺序不重要。
public Iterator<Item> iterator()
{ return new ListIterator();}
private class ListIterator implements Iterator<Item>
{
  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;
  }
}
}
```

1.4 算法分析

1.4.3 数学模型

  • 对于大多数程序,获得其运行时间的数据模型所需的步骤:

    • 肯定输入模型,定义问题的规模;
    • 识别内循环(执行最频繁的语句);
    • 根据内循环中的操做肯定成本模型;
    • 对于给定的输入,判断这些操做的执行频率。
    • 例:二分查找,它的输入模型是大小为N的数组a[],内循环是一个while循环中的全部语句,成本模型是比较操做(比较两个数组元素的值)
    • 白名单,它的输入模型是白名单的大小N和由标准输入获得的M个整数,且假设M>N,内循环是一个while循环中的全部语句,成本模型是比较操做(承自二分查找)

1.4.4 增加数量级的分类

  • 对增加数量级的常见假设的总结

clipboard.png

  • 2-sum NlogN解法(假设全部整数各不相同)

    • 若是二分查找不成功则会返回-1,不会增长计数器的值
    • 若是二分查找返回的 j > i,咱们就有a[i]+a[j]=0,增长计数器的值
    • 若是二分查找返回的j在0和i之间,不能增长计数器,避免重复计数。

      java 
      import java.util.Arrays; 
      public class TwoSumFast 
      { 
          public static int cout(int[] a) 
          { 
              Arrays.sort(a); 
              int N = a.length; 
              int cnt = 0; 
              for (int i = 0; i< N; i++) 
                  if (BinarySearch.rank(-a[i], a) > i) 
                      cnt++; 
              return cnt; 
          } 
      }
  • 3-sum N2logN解法(假设全部整数各不相同)

    import java.util.Arrays;
    public class ThreeSumFast
    {
      public static int cout(int[] a)
      {
        Arrays.sort(a);
        int N = a.length;
        int cnt = 0;
        for (int i = 0; i< N; i++)
            for(int j = i + 1;j < N; j++)
                if (BinarySearch.rank(-a[i]-a[j], a) > j)
                    cnt++;
        return cnt;
      }
    }

1.4.7 注意事项

  • 大常数:例如,当咱们取函数2N2+cN的近似为 2N2时,咱们的假设是c很小,若是c很大,该近似就是错误的。
  • 非决定性的内循环:
  • 指令时间:每条指令执行所需的时间老是相同的假设并不老是正确的。
  • 系统因素:计算机老是同时运行着许多程序
  • 不分伯仲:在咱们比较执行相同任务的两个程序时,经常出现的状况是其中一个在某些场景中更快而在另外一些场景中更慢。
  • 对输入的强烈依赖
  • 多个问题参数

1.4.8 处理对于输入的依赖

clipboard.png
clipboard.png

1.5 案例研究:union-find算法

  • 优秀的算法由于可以解决实际问题而变得更为重要;
  • 高效算法的代码也能够很简单;
  • 理解某个实现的性能特色是一项有趣而使人知足的挑战;
  • 在解决同一个问题的多种算法之间进行选择时,科学方法是一种重要的工具;
  • 迭代式改进可以让算法的效率愈来愈高。

1.5.1 动态链接性问题

  • 问题的输入是一列整数对,其中每一个整数都表示一个某种类型的对象,一对整数pq能够被理解为“p和q是相连的”,咱们假设相连是一种对等的关系。对等关系可以将对象分为多个等价类,在这里,当且仅当两个对象相连时它们才属于同一个等价类。咱们的目标是编写一个程序来过滤掉序列中全部无心义的整数对(两个整数均来自于同一个等价类中)。换句话说,当程序从输入中读取了证书对p q时,若是已知的全部整数对都不能说明p和q相连的,那么则将这一对整数写入到输出中。若是已知的数据能够说明p 和q是相连的,那么程序应该忽略p q继续处理输入中的下一对整数。

clipboard.png

  • 该问题可应用于:

    • 网络
    • 变量名等价性
    • 数据集合
  • 设计一份API封装所需的基本操做:初始化、链接两个触点、判断包含某个触点的份量、判断两个触点是否存在于同一个份量之中以及返回全部份量的数量。

clipboard.png

java 
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 < N;i++) 
        id[i] = i; 
    } 
    public int count() 
    { return count;} 
    public boolean connected(int p, int q) 
    { renturn find(p) == find(q); } 
    public int find(int p)//见quick-find 
    public void union(int p, int q)//见quick-union,加权quick-union 
    public static void main(String[] args) 
    {//解决由StdIn获得的动态连通性问题 
        int N = StdIn.readInt() //读取触点数量 
        UF N = new UF(N); //初始化N个份量 
        while (!StdIn.isEmpty()) 
        { 
            int p = StdIn.readInt(); 
            int q = StdIn.readInt();//读取整数对 
            if (uf.connected(p, q)) continue;//若是已经连通则忽略 
            uf.union(p, q);//归并份量 
            StdOut.println(p + " " + q);//打印链接 
        } 
        StdOut.println(uf.count() + "components"); 
    } 
}

1.5.2 实现(均根据以触点为索引的id[]数组来肯定两个触点是否存在于相同的连通份量中)

  • quick-find算法:保证当且仅当id[p]等于id[q]时p和q是连通的。换句话说,在同一个连通份量重的全部触点在id[]中的值必须所有相同。

    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);
    
    //若是p和q已经在相同的份量之中则不须要采起任何行动
    if (pID == qID) return;
    
    //将p的份量重命名为q的名称
    for (int i = 0;i < id.length; i++)
        if (id[i] == pID) id[i] = qID;
    count--;
    }

clipboard.png

  • find()操做的速度显然是很快的,由于它只须要访问id[]数组一次。但quick-find算法通常没法处理大型问题,由于对于每一对输入union()都须要扫描整个id[]数组。
  • quick-union算法:

    • 每一个触点所对应的id[]元素都是同一个份量中的另外一个触点的名称(也多是它本身)——咱们将这种联系称为连接
    • 在实现find()方法时,咱们从给定的触点开始,由它的连接获得另外一个触点,再由这个触点的连接到达第三个触点,如此继续指导到达一个根触点,即连接指向本身的触点。
    • 当且仅当分别由两个触点开始的这个过程到达同一个根触点时它们存在于同一个连通份量中。

      private 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--;
      }

clipboard.png

  • 加权 quick-union算法:记录每一棵树的大小并老是将较小的树链接到较大的树上。

    public class UF
    {
      private int[] id;//父连接数组(由触点索引)
      private int[] sz;//(有触点索引的)各个根节点所对应的份量的大小
      private int count; //连通份量的数量
      public WeightedQuickUnionUF(int N)
      {
        count = N;
        id = new int[N];
        for(int i=0;i < N;i++)
          id[i] = i;
        sz = new int[N];
        for(int i = 0; i < N; i++) sz[i] = 1;
      }
      public int count()
      { return count;}
      public boolean connected(int p, int q)
      { renturn find(p) == find(q); }
      public int find(int p)
      {//跟随连接找到根节点
        while(p != id[p]) p = id[p];
        return p;
      }
      public void union(int p, int q)
      {
        int i = find(p);
        int j = find(q);
        if(i == j) return;
        //将小树的根节点链接到大树的根节点
        if (sz[i] < sz[j]) { id[i] = j; sz[j] += sz[i];}
        else{id[j] = i;sz[i] += sz[j];}
        count--;
      }
    }
  • 最优算法

clipboard.png

相关文章
相关标签/搜索