一、问题引入
杨辉三角(Pascal’s Triangle)是组合数学中最经典的几何排列之一。它的每一行对应二项式展开的系数,从顶端开始,每个数等于上方两数之和。对于算法学习者来说,杨辉三角是理解动态规划递推思想的绝佳入门案例。
本文将用Java实现两种生成杨辉三角的方法:动态规划递推法和组合数公式直接计算法,并对比它们的时空复杂度。
二、算法原理
2.1 动态规划递推法
杨辉三角的核心递推关系非常简单:
– 第0行只有1
– 每行的首尾元素都是1
– 中间元素 C(i,j) = C(i-1,j-1) + C(i-1,j)
这个递推式天然符合动态规划的定义:当前状态由之前的状态推导而来。
2.2 组合数公式法
根据组合数学定义,杨辉三角第i行第j列的元素可以直接用组合数公式计算:
C(i,j) = i! / (j! * (i-j)!)
为了避免阶乘溢出,可以采用递推计算组合数:
C(i,j) = C(i,j-1) * (i-j+1) / j
三、Java代码实现
import java.util.ArrayList;
import java.util.List;
/**
* 杨辉三角生成器
* 包含动态规划递推法和组合数公式法两种实现
*/
public class PascalTriangle {
/**
* 方法1:动态规划递推法
* 时间复杂度:O(numRows^2)
* 空间复杂度:O(numRows^2)(存储结果)
*
* @param numRows 要生成的行数
* @return 杨辉三角的二维列表
*/
public List<List<Integer>> generateByDP(int numRows) {
List<List<Integer>> result = new ArrayList<>();
if (numRows <= 0) {
return result;
}
// 第0行总是[1]
List<Integer> firstRow = new ArrayList<>();
firstRow.add(1);
result.add(firstRow);
// 从第1行开始递推
for (int i = 1; i < numRows; i++) {
List<Integer> prevRow = result.get(i - 1);
List<Integer> currentRow = new ArrayList<>();
// 每行第一个元素是1
currentRow.add(1);
// 中间元素 = 上一行左上方 + 上一行正上方
for (int j = 1; j < i; j++) {
int val = prevRow.get(j - 1) + prevRow.get(j);
currentRow.add(val);
}
// 每行最后一个元素是1
currentRow.add(1);
result.add(currentRow);
}
return result;
}
/**
* 方法2:组合数公式法(递推计算避免阶乘溢出)
* 时间复杂度:O(numRows^2)
* 空间复杂度:O(numRows^2)
*
* @param numRows 要生成的行数
* @return 杨辉三角的二维列表
*/
public List<List<Integer>> generateByCombination(int numRows) {
List<List<Integer>> result = new ArrayList<>();
for (int i = 0; i < numRows; i++) {
List<Integer> row = new ArrayList<>();
long combination = 1; // 使用long防止中间计算溢出
for (int j = 0; j <= i; j++) {
if (j == 0) {
combination = 1;
} else {
// C(i,j) = C(i,j-1) * (i-j+1) / j
combination = combination * (i - j + 1) / j;
}
row.add((int) combination);
}
result.add(row);
}
return result;
}
/**
* 获取杨辉三角中指定位置的值(空间优化版)
* 只使用O(numRows)额外空间生成最后一行
*
* @param rowIndex 行索引(从0开始)
* @return 该行的列表
*/
public List<Integer> getRow(int rowIndex) {
List<Integer> row = new ArrayList<>();
// 利用组合数递推公式,从后往前更新,避免覆盖
for (int i = 0; i <= rowIndex; i++) {
row.add(1); // 先扩展列表
// 从倒数第二个元素开始更新
for (int j = i - 1; j > 0; j--) {
row.set(j, row.get(j) + row.get(j - 1));
}
}
return row;
}
/**
* 打印杨辉三角(格式化对齐)
*/
public void printTriangle(List<List<Integer>> triangle) {
int n = triangle.size();
int maxVal = triangle.get(n - 1).get(n / 2);
int maxWidth = String.valueOf(maxVal).length();
int rowWidth = (n * 2 - 1) * maxWidth;
for (int i = 0; i < n; i++) {
List<Integer> row = triangle.get(i);
int spaces = (rowWidth - row.size() * maxWidth) / 2;
// 打印前导空格
for (int s = 0; s < spaces; s++) {
System.out.print(" ");
}
for (int val : row) {
System.out.printf("%" + maxWidth + "d ", val);
}
System.out.println();
}
}
public static void main(String[] args) {
PascalTriangle solution = new PascalTriangle();
int numRows = 10;
System.out.println("=== 动态规划递推法 ===");
List<List<Integer>> dpResult = solution.generateByDP(numRows);
solution.printTriangle(dpResult);
System.out.println("\n=== 组合数公式法 ===");
List<List<Integer>> combResult = solution.generateByCombination(numRows);
solution.printTriangle(combResult);
System.out.println("\n=== 空间优化获取第" + numRows + "行 ===");
List<Integer> row = solution.getRow(numRows - 1);
System.out.println(row);
}
}
四、复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 | 特点 |
|---|---|---|---|
| 动态规划递推法 | O(n²) | O(n²) | 直观易懂,适合理解递推思想 |
| 组合数公式法 | O(n²) | O(n²) | 体现数学公式与编程的结合 |
| 空间优化版 | O(n²) | O(n) | 只保存当前行,适合只需要单行结果的场景 |
五、算法应用
杨辉三角不仅是数学上的优美结构,在算法中也有广泛应用:
– 组合数查询:杨辉三角的第n行第k个数即为C(n,k)
– 概率计算:二项分布的概率质量函数直接使用组合数
– 路径计数:网格中从起点到终点的路径数问题
六、总结
杨辉三角虽然看似简单,但它涵盖了动态规划的核心思想:状态定义 + 状态转移方程。通过两种实现方式的对比,读者可以体会到递推与数学公式之间的等价关系,以及空间优化的技巧。这是学习更复杂动态规划问题的理想起点。