袋熊的树洞

日拱一卒,功不唐捐

0%

Coordinate Descent

在机器学习算法中,我们经常需要解一些优化问题,优化算法有很多,其中坐标下降法 (Coordinate Descent) 是一个很经典的算法,坐标下降法主要思想是每次迭代,只对一个坐标轴方向进行优化,其他坐标轴的值固定不变,使得多变量优化问题变成单变量优化问题。例如需要解决以下优化问题

$$
\min_{x} f(x_1, x_2, \cdots, x_d)
$$

其中 $x = (x_1, x_2, \cdots, x_d)^T \in R^d$。用坐标下降法解决该问题时,大概流程如下:

固定其他变量 $x_i (i\ne j)$ 不变,选择一个变量 $x_j$ 进行优化,然后更新该变量的值,接着选择下一个坐标轴方向进行优化,直到算法收敛。在算法中,选择坐标轴 $j$ 的方法有很多,可以随机选取,也可以对所有坐标轴循环一遍。

Lasso问题求解

Lasso优化问题通常如下表示:

$$
\min_{\beta} \frac{1}{2} \Vert y - X\beta \Vert^2_2 + \lambda \Vert \beta \Vert_1
$$

其中 $y \in R^{n}$,$X \in R^{n \times p}$,$\beta \in R^p$。令损失函数表示为 $f(\beta) = f(\beta_1, \cdots, \beta_p)$,即

$$
\begin{align}
f(\beta) &= \frac{1}{2} \Vert y - X\beta \Vert^2_2 + \lambda \Vert \beta \Vert_1 \
& = \frac{1}{2} \sum_{i=1}^n (y_i - \sum_{j=1}^p x_{ij} \beta_j)^2 + \lambda \sum_{j=1}^p \vert \beta_j \vert
\end{align}
$$

根据前面描述的坐标下降算法,要求第 $k$ 坐标轴方向的最优值,即求
$$
\min_{\beta_k} f(\beta_1, \cdots, \beta_{k-1}, \beta_k, \beta_{k+1}, \cdots, \beta_p)
$$
其中 $\beta_{j} {(j\ne k)}$ 已知且值固定不变。要求解该问题,可以通过梯度等于0来求解。对 $f(\beta)$ 求偏导可以得到:
$$
\begin{align}
\frac{\partial{f(\beta)}}{\partial{\beta_k}} & = -\sum_{i=1}^n (y_i - \sum_{j=1}^p x_{ij} \beta_j) x_{ik} + \frac{\partial{(\lambda \vert \beta_k \vert)}}{\partial{\beta_k}} \
& =- \sum_{i=1}^n (y_i - \sum_{j \ne k} x_{ij} \beta_j)x_{ik} + \sum_{i=1}^n x_{ik}^2 \beta_k + \frac{\partial{(\lambda \vert \beta_k \vert)}}{\partial{\beta_k}} \
& = - r_k + z_k \beta_k + \frac{\partial{(\lambda \vert \beta_k \vert)}}{\partial{\beta_k}}
\end{align}
$$
其中 $r_k =\sum_{i=1}^n (y_i - \sum_{j \ne k} x_{ij} \beta_j)x_{ik}$,$z_k = \sum_{i=1}^n x_{ik}^2$,对于 $\frac{\partial{(\lambda \vert \beta_k \vert)}}{\partial{\beta_k}}$,这里需要引入 subgradient 概念,具体内容可以参考这篇文章

$$
\frac{\partial{(\lambda \vert \beta_k \vert)}}{\partial{\beta_k}} = \begin{cases}
\lambda & \beta_k \gt 0 \
[-\lambda, \lambda] & \beta_k = 0 \
-\lambda & \beta_k \lt 0
\end{cases}
$$

则可以得到 $f(\beta)$ 的偏导为:

$$
\frac{\partial{f(\beta)}}{\partial{\beta_k}} = \begin{cases}
-r_k + z_k \beta_k + \lambda & \beta_k \gt 0 \
[-r_k - \lambda, -r_k + \lambda] & \beta_k = 0 \
-r_k + z_k \beta_k - \lambda & \beta_k \lt 0
\end{cases}
$$

令 $\frac{\partial{f(\beta)}}{\partial{\beta_k}} = 0$,可以得到极小值点 $\beta_k^*$ ,由于 $\frac{\partial{f(\beta)}}{\partial{\beta_k}}$ 是个分段函数,需要分类讨论:

  • 当 $\beta_k \gt 0$ 时
    $$
    \begin{align}
    & -r_k + z_k \beta_k^* + \lambda = 0 \
    \Rightarrow \quad & \beta_k^* = \frac{r_k - \lambda}{z_k}
    \end{align}
    $$
    由于 $\beta_k \gt 0$,则 $\beta_k^* = \frac{r_k - \lambda}{z_k} \gt 0$,即 $r_k \gt \lambda$

  • 当 $\beta_k \lt 0$ 时
    $$
    \begin{align}
    & -r_k + z_k \beta_k^* - \lambda = 0 \
    \Rightarrow \quad & \beta_k^* =\frac{ r_k + \lambda}{z_k}
    \end{align}
    $$
    由于 $\beta_k \lt 0$,则 $\beta_k^* = r_k + \lambda \lt 0$,即 $r_k \lt -\lambda$

  • 当 $\beta_k = 0$ 时,可以得到 $0 \in [-r_k - \lambda, -r_k + \lambda]$,则可以得到
    $$
    \begin{cases}
    -r_k - \lambda \le 0 \
    -r_k + \lambda \ge 0
    \end{cases} \Rightarrow -\lambda \le r_k \le \lambda
    $$

综上所有内容,我们可以得到第 $k$ 个坐标轴方向的最优值为:
$$
\beta_k^* = \begin{cases}
\frac{r_k - \lambda}{z_k} & r_k \gt \lambda \
0 & -\lambda \le r_k \le \lambda \
\frac{r_k + \lambda}{z_k} & r_k \lt -\lambda
\end{cases}
$$

定义一个函数 $S(a, \lambda)$,这个函数也称为 soft thresholding function
$$
S(a, \lambda) = \begin{cases}
a - \lambda & a \gt \lambda \
0 & -\lambda \le a \le \lambda \
a + \lambda & a \lt -\lambda
\end{cases}
$$
所以 $\beta_k^* = \frac{1}{z_k}S(r_k, \lambda)$,$k = 1, 2, \ldots, n$。这个函数也可以写成另一种形式
$$
S(a, \lambda) = (a-\lambda){+} - (-a - \lambda){+}
$$
这里 $(x)_{+} = \max(x, 0)$。

变种问题

由于Lasso优化问题的表示有很好几种,不同表示的优化问题,对应解的表示也会有所不同,虽然没什么大变化,但是还是罗列一下。

变种一

Lasso优化问题表示如下:
$$
\min_{\beta} \Vert y - X\beta \Vert^2_2 + \lambda \Vert \beta \Vert_1
$$
令 $f(\beta) = \Vert y - X \beta \Vert_2^2 + \lambda \Vert \beta \Vert_1$,则 $\frac{\partial{f(\beta)}}{\partial{\beta_k}}$ 为:
$$
\begin{align}
\frac{\partial{f(\beta)}}{\partial{\beta_k}} & = -2 \sum_{i=1}^n (y_i - \sum_{j=1}^p x_{ij} \beta_j) x_{ik} + \frac{\partial{(\lambda \vert \beta_k \vert)}}{\partial{\beta_k}} \
& =- 2 \sum_{i=1}^n (y_i - \sum_{j \ne k} x_{ij} \beta_j)x_{ik} + 2 \sum_{i=1}^n x_{ik}^2 \beta_k + \frac{\partial{(\lambda \vert \beta_k \vert)}}{\partial{\beta_k}} \
& = - 2 r_k + 2z_k \beta_k + \frac{\partial{(\lambda \vert \beta_k \vert)}}{\partial{\beta_k}}
\end{align}
$$
其中 $r_k = \sum_{i=1}^n (y_i - \sum_{j \ne k} x_{ij} \beta_j)x_{ik}$。使用 subgradient 可以进一步得到
$$
\frac{\partial{f(\beta)}}{\partial{\beta_k}} = \begin{cases}

  • 2r_k + 2 z_k \beta_k + \lambda & \beta_k \gt 0 \
    [-2r_k - \lambda, - 2 r_k + \lambda] & \beta_k = 0 \
  • 2r_k + 2 z_k \beta_k - \lambda & \beta_k \lt 0
    \end{cases}
    $$

根据前面讨论,令偏导为0,求得极值点 $\beta_k^*$ 为

$$
\beta_k^* = \begin{cases}
\frac{2r_k - \lambda}{2z_k} & 2r_k \gt \lambda \
0 & -\lambda \le 2r_k \le \lambda \
\frac{2r_k + \lambda}{2z_k} & 2r_k \lt -\lambda
\end{cases}
$$

用 soft thresholding function 表示为 $\beta_k^* = \frac{1}{2z_k} S(2r_k, \lambda)$

变种二

Lasso优化问题表示如下:
$$
\min_{\beta} \Vert X\beta - y\Vert^2_2 + \lambda \Vert \beta \Vert_1
$$

令 $f(\beta) = \Vert X \beta - y \Vert^2_2 + \lambda \Vert \beta \Vert_1$,则 $\frac{\partial{f(\beta)}}{\partial{\beta_k}}$ 为:
$$
\begin{align}
\frac{\partial{f(\beta)}}{\partial{\beta_k}} & = 2 \sum_{i=1}^n (\sum_{j=1}^p x_{ij} \beta_j - y_i) x_{ik} + \frac{\partial{(\lambda \vert \beta_k \vert)}}{\partial{\beta_k}} \
& = 2 \sum_{i=1}^n (\sum_{j \ne k} x_{ij} \beta_j - y_i)x_{ik} + 2 \sum_{i=1}^n x_{ik}^2 \beta_k + \frac{\partial{(\lambda \vert \beta_k \vert)}}{\partial{\beta_k}} \
& = 2 r_k + 2z_k \beta_k + \frac{\partial{(\lambda \vert \beta_k \vert)}}{\partial{\beta_k}}
\end{align}
$$
其中 $r_k = \sum_{i=1}^n (\sum_{j \ne k} x_{ij} \beta_j - y_i)x_{ik}$。使用 subgradient 可以进一步得到
$$
\frac{\partial{f(\beta)}}{\partial{\beta_k}} = \begin{cases}
2r_k + 2 z_k \beta_k + \lambda & \beta_k \gt 0 \
[2r_k - \lambda, 2 r_k + \lambda] & \beta_k = 0 \
2r_k + 2 z_k \beta_k - \lambda & \beta_k \lt 0
\end{cases}
$$
根据前面讨论,令偏导为0,求得极值点 $\beta_k^*$ 为
$$
\beta_k^* = \begin{cases}
\frac{-2r_k - \lambda}{2z_k} & -2r_k \gt \lambda \
0 & -\lambda \le -2r_k \le \lambda \
\frac{-2r_k + \lambda}{2z_k} & -2r_k \lt -\lambda
\end{cases}
$$

用 soft thresholding function 表示为 $\beta_k^* = \frac{1}{2z_k} S(-2r_k, \lambda)$

变种三

Lasso优化问题表示如下:
$$
\min_{\beta} \frac{1}{2 n} \Vert y - X\beta \Vert^2_2 + \lambda \Vert \beta \Vert_1
$$
同前面的推导,这里直接给出结果
$$
\beta_k^* = \begin{cases}
\frac{r_k - \lambda}{z_k} & r_k \gt \lambda \
0 & -\lambda \le r_k \le \lambda \
\frac{r_k + \lambda}{z_k} & r_k \lt -\lambda
\end{cases}
$$
其中 $r_k =\frac{1}{n} \sum_{i=1}^n (y_i - \sum_{j \ne k} x_{ij} \beta_j)x_{ik}$,$z_k = \frac{1}{n} \sum_{i=1}^n x_{ik}^2$

偏置 bias 求解

前面讨论的模型不包括偏置,如果模型包括偏置 $b$,则优化问题写为
$$
\min_{\beta, b} \frac{1}{2} \Vert y - X\beta - 1b \Vert^2_2 + \lambda \Vert \beta \Vert_1
$$
这里的 $1$ 代表元素全为 1 的向量,大小为 $R^n$,损失函数 $f(\beta, b)$ 为
$$
\begin{align}
f(\beta, b) &= \frac{1}{2} \Vert y - X\beta - 1 b\Vert^2_2 + \lambda \Vert \beta \Vert_1 \
& = \frac{1}{2} \sum_{i=1}^n (y_i - \sum_{j=1}^p x_{ij} \beta_j - b)^2 + \lambda \sum_{j=1}^p \vert \beta_j \vert
\end{align}
$$
则对 $f(\beta, b)$ 求偏导 $\beta_k$ 可以得到
$$
\begin{align}
\frac{\partial{f(\beta, b)}}{\partial{\beta_k}} & = -\sum_{i=1}^n (y_i - \sum_{j=1}^p x_{ij} \beta_j - b) x_{ik} + \frac{\partial{(\lambda \vert \beta_k \vert)}}{\partial{\beta_k}} \
& =- \sum_{i=1}^n (y_i - \sum_{j \ne k} x_{ij} \beta_j - b)x_{ik} + \sum_{i=1}^n x_{ik}^2 \beta_k + \frac{\partial{(\lambda \vert \beta_k \vert)}}{\partial{\beta_k}} \
& = - r_k + z_k \beta_k + \frac{\partial{(\lambda \vert \beta_k \vert)}}{\partial{\beta_k}}
\end{align}
$$
其中 $r_k =\sum_{i=1}^n (y_i - \sum_{j \ne k} x_{ij} \beta_j -b)x_{ik}$,$z_k = \sum_{i=1}^n x_{ik}^2$,令 $\frac{\partial{f(\beta, b)}}{\partial{\beta_k}} = 0$,可以得到
$$
\beta_k^* = \begin{cases}
\frac{r_k - \lambda}{z_k} & r_k \gt \lambda \
0 & -\lambda \le r_k \le \lambda \
\frac{r_k + \lambda}{z_k} & r_k \lt -\lambda
\end{cases}
$$
对 $f(\beta, b)$ 求偏导 $b$ 可以得到
$$
\begin{align}
\frac{\partial{f(\beta, b)}}{\partial{b}} & = -\sum_{i=1}^n (y_i - \sum_{j=1}^p x_{ij} \beta_j - b) \
& = n b - \sum_{i=1}^n (y_i - \sum_{j=1}^p x_{ij} \beta_j)
\end{align}
$$
令 $\frac{\partial{f(\beta, b)}}{\partial{b}} = 0$,可以得到
$$
b^* = \frac{1}{n} \sum_{i=1}^n (y_i - \sum_{j=1}^p x_{ij} \beta_j)
$$

算法实现

在本节,要解决的优化问题如下表示

$$
\min_{\beta, b} \frac{1}{2 n} \Vert y - X\beta - 1b\Vert^2_2 + \lambda \Vert \beta \Vert_1
$$
根据前面的讨论,在选定第 $k$ 坐标轴方向,解 $\beta^*_k$的表达式为
$$
\beta_k^* = \begin{cases}
\frac{r_k - \lambda}{z_k} & r_k \gt \lambda \
0 & -\lambda \le r_k \le \lambda \
\frac{r_k + \lambda}{z_k} & r_k \lt -\lambda
\end{cases}
$$

其中 $r_k =\frac{1}{n} \sum_{i=1}^n (y_i - \sum_{j \ne k} x_{ij} \beta_j - b)x_{ik}$,$z_k = \frac{1}{n} \sum_{i=1}^n x_{ik}^2$,解 $b^*$ 的表达式为
$$
b^* = \frac{1}{n} \sum_{i=1}^n (y_i - \sum_{j=1}^p x_{ij} \beta_j)
$$

这里可以将 $r_k$ 写成向量化形式,由于算法每次只更新一个坐标轴方向的值,即

$$
\begin{align}
& [\beta_1, \ldots, \beta_{k-1}, \beta_{k}, \beta_{k+1}, \ldots, \beta_p] \
\Rightarrow ; & [\beta_1, \ldots, \beta_{k-1}, \beta_{k}^*, \beta_{k+1}, \ldots, \beta_p]
\end{align}
$$
因此 $r_k$ 可以写为
$$
\begin{align}
r_k & =\frac{1}{n} \sum_{i=1}^n (y_i - \sum_{j =1}^p x_{ij} \beta_j - b + x_{ik} \beta_k)x_{ik}
\end{align}
$$
这里的 $\beta_k$ 是未更新之前的值,更新后的值为 $\beta_k^*$,注意区别。用向量表示 $r_k$ 为
$$
r_k = \frac{1}{n} (X[:, k])^T (y - X\beta - 1 b + \beta_k X[:, k])
$$

其中 $X[:, k]$ 代表矩阵 $X$ 第 $k$ 列。$b^*$ 的表达式写成向量形式可以写为
$$
b^* = \frac{1}{n} 1^T (y - X\beta)
$$
算法整体流程如下表示

核心代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
class Lasso:
"""
The optimization objective of Lasso is
1/(2 * n) * ||y - Xw||^2_2 + alpha * ||w||_1
"""

def __init__(self, alpha=1.0, fit_intercept=True, normalize=False,
copy_X=True, max_iter=1000, tol=0.0001, selection='cyclic'):
self._alpha = alpha
self._fit_intercept = fit_intercept
self._normalize = normalize
self._copy_X = copy_X
self._max_iter = max_iter
self._tol = tol
self._selection = selection

if normalize:
self._scaler = preprocessing.StandardScaler()

def compute_step(self, k, X, y, coef, intercept, alpha):
n, p = X.shape
y_predict = np.dot(X, coef) + intercept
rk = np.dot(X[:, k], y - y_predict + X[:, k] * coef[k])
rk = rk / (1.0 * n)
zk = np.linalg.norm(X[:, k], ord=2) ** 2
zk = zk / (1.0 * n)
coef_k = (np.amax([rk-alpha, 0]) - np.amax([-rk-alpha, 0]))
coef_k = coef_k / (1.0 * zk)

return coef_k

def objective(self, X, y, coef, intercept, alpha):
n, p = X.shape
total = 0

y_predict = np.dot(X, coef) + intercept
total += \
1/(2.0*n) * np.linalg.norm(y-y_predict, ord=2) ** 2
total += alpha * np.linalg.norm(coef, ord=1)

return total

def fit(self, X, y):
if self._copy_X:
X = X.copy()
if self._normalize:
X = self._scaler.fit_transform(X)
self._objectives = []

# initialize data
num_samples, num_features = X.shape
coef = np.zeros(num_features)
old_coef = np.zeros(num_features)
intercept = 0
if self._fit_intercept:
tmp = y - np.dot(X, coef)
intercept = np.sum(tmp) / (1.0 * num_samples)
num_iters = 0
for iter in range(self._max_iter):
num_iters = num_iters + 1
if (self._selection == 'cyclic'):
for k in range(num_features):
old_coef[k] = coef[k]
coef[k] = self.compute_step(k, X, y, coef,
intercept, self._alpha)
if self._fit_intercept:
tmp = y - np.dot(X, coef)
intercept = np.sum(tmp) / (1.0 * num_samples)

# check condition of convergence
coef_updates = np.abs(coef - old_coef)
if np.amax(coef_updates) < self._tol:
break
self._objectives.append(self.objective(X, y, coef,
intercept, self._alpha))

self._coef = coef
self._intercept = intercept
self._num_iters = num_iters

return self

def predict(self, X):
if self._copy_X:
X = X.copy()
if self._normalize:
X = self._scaler.transform(X)

y_predict = np.dot(X, self._coef) + self._intercept

return y_predict

def score(self, X, y):
y_predict = self.predict(X)

return r2_score(y, y_predict)

@property
def coef_(self):
return self._coef

@property
def intercept_(self):
return self._intercept

@property
def n_iter_(self):
return self._num_iters

@property
def objectives_(self):
return self._objectives

def __str__(self):
return ('Lasso(alpha={}, copy_X={}, '
'fit_intercept={}, max_iter={}, '
'normalize={}, selection=\'{}\', '
'tol={})').format(self._alpha, self._copy_X,
self._fit_intercept, self._max_iter,
self._normalize, self._selection,
self._tol)

相关代码在:GitHub

参考

  1. A COORDINATE DESCENT ALGORITHM FOR THE LASSO PROBLEM
  2. SOFT-THRESHOLDING OPERATOR AND THE LASSO SOLUTION

题目一 找到差值为k的数字对的个数

问题描述

在n个元素的数组中,找到差值为k的数字对去重后的个数

输入描述:

1
2
第一行,n和k,n表示数字个数,k表示差值
第二行,n个正整数

输出描述:

1
差值为k的数字对去重后的个数

示例 1

输入

1
2
5 2
1 5 3 4 2

输出

1
3

说明

1
其中数字对分别为 (1, 3), (5, 3), (4, 2)

示例 2

输入

1
2
6 2
1 5 3 3 4 2

输出

1
3

说明

1
其中数字对分别为 (1, 3), (5, 3), (4, 2)

示例 3

输入

1
2
4 0
1 1 1 1

输出

1
1

说明

1
其中数字对分别为 (1, 1)

备注:

1
2
3
4
5
6
数据范围:
对于 100% 的数据,0 <= k < 100
对于 100% 的数据,数组内数值范围在 [0, 100000000]
对于 30% 的数据,0 < n < 1000
对于 80% 的数据,0 < n < 100000
对于 100% 的数据,0 < n < 1000000

解决方案

这道题目在 LeetCode 有相应的题目,题目为 532. K-diff Pairs in an Array,这道题目我在另一篇博文做了解答,可以参考 LeetCode 532 - K-diff Pairs in an Array

题目二 求函数调用次数

问题描述

定义两个字符串变量:s 和 m,再定义两种操作。

第一种操作:

1
2
m = s;
s = s + s;

第二种操作:

1
s = s + m;

假设 s, m 初始化如下:

1
2
s = "a";
m = s;

求最小的操作步骤数,可以将 s 拼接到长度等 n

输入描述:

1
2
一个整数n,表明我们需要得到s字符串长度
对于100%的数据,0 < n < 10000

输出描述:

1
一个整数,表明总共操作次数

示例 1

输入

1
6

输出

1
3

说明

1
输入是6,表明我们需要得到s字符串长度为6,也就是s最终为"aaaaaa",那么依次使用2次"第一次操作“和1次"第二种操作"就能达到目的,总共操作次数是3。

示例2

输入

1
5

输出

1
4

说明

1
假设n=5,表明我们需要得到s字符串长度为5,也就是s最终为"aaaaa",那么直接使用4次"第二种操作“就能达到目的,总共操作次数是4。

示例3

输入

1
4

输出

1
2

说明

1
假设n=4,表明我们需要得到s字符串长度为4,也就是s最终为"aaaa",那么直接使用2次"第一种操作"就能达到目的,总共操作次数是2。

题目三

没记录到。。。

题目四 Magic

问题描述

给一个包含n个整数元素的集合a,一个包含m个整数元素的集合b。定义magic操作为,从一个集合中取出一个元素,放到另一个集合里,且操作过后每个集合的平均值大于操作前。注意以下两点:

  1. 不可以把一个集合的元素取空,这样就没有平均值了
  2. 值为x的元素从集合b取出放入集合a,但集合a中已有值为x的元素,则a的平均值不变 (因为集合元素不会重复),b的平均值可能会改变 (因为x被取出了)

问最多可以进行多少次magic操作?

输入描述:

1
2
3
4
5
6
第一行为两个整数n,m
第二行n个整数,表示集合a中的元素
第三行m个整数,表示集合b中的元素
对于30%的数据,最终结果 <= 1
对于70%的数据,输入中的a,b集合元素完全没有重复,即 |a| + |b| = |a 并 b|
对于100%的数据,1 < n, m < 100,000,0 < a[i], b[i] < 100,000,000,集合a中元素互不相同,集合b中元素互不相同

输出描述:

1
输出一个整数,表示最多可以进行的操作次数。

示例 1

输入

1
2
3
3 5
1 2 5
2 3 4 5 6

输出

1
2

说明

1
依次从b集合取出3,4元素放入a集合

题目五 跳板小游戏

问题描述

小T最近迷上了一款跳板小游戏。已知空中有N个高度互不相同的跳板,小T刚开始在高度为0的地方,每次跳跃可以选择与自己当前高度绝对值差小于等于H的跳板,跳跃过后到达以跳板为轴的镜像位置,问小T在最多跳K次的情况下最高能跳多高? (任意时刻,高度不能为负)

输入描述:

1
2
第一行三个整数N,K,H
以下N行,每行一个整数Ti,表示第i个跳板的离地高度

输出描述:

1
一个整数,表示最高能跳到的高度

示例 1

输入

1
2
3
4
3 3 2
1
3
6

输出

1
8

说明

1
2
3
4
小T初始在高度为0的地方
第一次跳跃只能选择高度为1的跳板,结束后到达高度为2的地方,计算方式:高度1 = 初始高度 + (跳板1高度 - 初始高度)*2 = 0 + (1-0)*2 = 2
第二次跳跃选择高度为3的跳板,结束后到达高度为4的地方,计算方式:高度2 = 高度1 + (跳板2高度 - 高度1)*2 = 2 + (3-2)*2 = 4
第三次跳跃选择高度为6的跳板,结束后到达高度为8的地方,计算方式:高度3 = 高度2 + (跳板3高度 - 高度2)*2 = 4 + (6-4)*2 = 8

备注:

1
2
对于40%的数据,1 <= N, K, H, Ti <= 20
对于100%的数据,1 <= N, K, Ti <= 100,000, 1 <= H <= 100

问题描述

Given an array of integers and an integer k, you need to find the number of unique k-diff pairs in the array. Here a k-diff pair is defined as an integer pair (i, j), where i and j are both numbers in the array and their absolute difference is k.

Example 1

Input: [3, 1, 4, 1, 5], k = 2

Output: 2

Explanation: There are two 2-diff pairs in the array, (1, 3) and (3, 5).
Although we have two 1s in the input, we should only return the number of unique pairs.

Example 2

Input:[1, 2, 3, 4, 5], k = 1

Output: 4

Explanation: There are four 1-diff pairs in the array, (1, 2), (2, 3), (3, 4) and (4, 5).

Example 3

Input: [1, 3, 1, 5, 4], k = 0

Output: 1

Explanation: There is one 0-diff pair in the array, (1, 1).

Note:

  1. The pairs (i, j) and (j, i) count as the same pair.
  2. The length of the array won’t exceed 10,000.
  3. All the integers in the given input belong to the range: [-1e7, 1e7].

解决方案

这道题目主要意思是找出差值为 k 的数字对,而且数字对是不重复,例如数字对 (1, 3) 和数字对 (3, 1) 是重复的。首先判断数字 a 和数字 b 的差值是否为 k,对于数字 a 和数字 b 的差值,可以用 $\vert a - b \vert$表示,当差值为 k 时,即 $\vert a - b \vert = k$,去掉绝对值符号可以得到
$$
\begin{align}
& a = b - k \
\text{or} \quad & a = b + k
\end{align}
$$
也就说知道数字 a,可以利用上面等式判断一个数字 b 与数字 a 的差值是否为 k。接下来解决重复数字对问题,对于带有重复数字对序列 (1, 3), (3, 1), (3, 5), (5, 3),我们可以用一个无重复元素容器,例如STL中setmapunorder_setunorder_map,保存每一个数字对的最小元素,即 1, 3, 5,然后容器元素个数就是不重复数字对的个数。

综合上面的讨论,我们可以得到一个思路,首先创建两个容器,第一个容器保存访问过的元素,第二个容器为无重复元素容器,保存已找到的数字对的最小的元素,然后遍历数组的每一个元素,令当前访问的元素为 b,

  • 如果 $a = b-k$ 在第一个容器中,保存 $\min(a, b) = b - k$ 到第二个容器中
  • 如果 $a = b + k$ 在第一个容器中,保存 $\min(a, b) = b$ 到第二个容器中
  • 如果上述条件都不满足,说明没有找到相应的数字对,将元素 $b$ 放入到第一个容器中

由于第一个容器要进行不断地查找,所以可以考虑使用哈希表或者二叉树之类的数据结构保存。基于上面思路,编写代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <vector>
#include <unordered_set>
using namespace std;

class Solution {
public:
int findPairs(vector<int>& nums, int k) {
if (k < 0)
return 0;

int num;
unordered_set<int> pair_smalls;
unordered_set<int> unique_nums;
for (int i=0; i<nums.size(); i++) {
num = nums[i];
if (unique_nums.find(num - k) != unique_nums.end())
pair_smalls.insert(num - k);
if (unique_nums.find(num + k) != unique_nums.end())
pair_smalls.insert(num);

unique_nums.insert(num);
}

return pair_smalls.size();
}
};

int main()
{
Solution solu;

vector<int> nums1 = {3, 1, 4, 1, 5};
int k1 = 2;
cout << solu.findPairs(nums1, k1) << endl;

vector<int> nums2 = {1, 2, 3, 4, 5};
int k2 = -1;
cout << solu.findPairs(nums2, k2) << endl;

return 0;
}

题目一 在字符串中找出连续最长的数字串

问题描述

给定一个字符串,输出字符串中最长的数字串,并把这个数字串的长度输出。

请在一个字符串中找出连续最长的数字串,并把这个串的长度返回;如果存在长度相同的连续数字串,返回最后一个连续数字串。

__注意__:数字串只需要是数字组成的就可以,并不要求顺序,比如数字串”1234”的长度就小于数字串”1359055”,如果没有数字,就返回空字符串 (“”) 而不是NULL。

输入描述

1
一个字符串

输出描述

1
2
输出最长的数字串,输出最长数字串中数字个数
中间以逗号 (,) 隔开

示例 1

输入

1
abcd12345ed125ss123058789

输出

1
123058789,9

备注

  1. 如果存在长度相同的连续数字串,则输出最后一个连续数字串。
  2. 数字串只需要是数字组成的就可以,并不要求顺序,比如数字串”1234”的长度就小于数字串”1359055”。
  3. 如果没有数字,就返回空字符串 (“”) 而不是NULL。

解决方案

可以设定两对指针,每对指针包含字符串的起始位置和结束位置的后一个位置,第一对指针保存着最长的数字串,第二对指针保存当前找到的数字串,通过指针相减可以得到字符串长度,通过比对长度大小不断更新指针的位置。实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <iostream>
#include <string>
using namespace std;

int main()
{
string str;
while (cin >> str) {
bool numeric;
int long_start, long_end, long_len;
int tmp_start, tmp_end;

long_start = long_end = -1;
long_len = 0;
tmp_start = tmp_end = 0;

numeric = false;
int i;
for (i=0; i<str.size(); i++) {
if (str[i] >= '0' && str[i] <= '9') {
if (numeric == false)
tmp_start = i;
numeric = true;
} else {
if (numeric == true) {
tmp_end = i;
if ((tmp_end - tmp_start) >= long_len) {
long_start = tmp_start;
long_end = tmp_end;
long_len = tmp_end - tmp_start;
}
}
numeric = false;
}
}
if (i == str.size() && numeric == true) {
tmp_end = i;
if ((tmp_end - tmp_start) >= long_len) {
long_start = tmp_start;
long_end = tmp_end;
long_len = tmp_end - tmp_start;
}
}

if (long_len > 0) {
for (i=long_start; i<long_end; i++)
cout << str[i];
cout << "," << long_len << endl;
} else {
cout << ",0" << endl;
}
}

return 0;
}

题目二 字节流解析

问题描述

根据数值占用BIT数,按顺序从输入字节流中解析出对应数值,解析顺序按输入数组astElement索引升序

1
2
void Decode(unsigned int uiInputLen, unsigned char aInputByte[],
unsigned int uiElementNum, ELEMENT_STRU astElement[]);

其中

1
2
3
4
5
6
7
8
9
unsigned int uiInputLen : 字节数组 (流) 长度
unsigned char aInputByte : 字节数组 (流)
unsigned int uiElementNum : 解析数值个数
ELEMENT_STRU astElement[] : 数值的结构数组指针,含义如下
Struct
{
unsigned int uiElementLength; //表示uiElementValue占用BIT数,范围1~32
unsigned int uiElementValue; //从字节流中按顺序解析的数值,用于输出
} ELEMENT_STRU;

输入描述

1
2
3
4
5
字节数组长度uiInputLen为3;
字节数组aInputByte[3]为{0x62, 0x80, 0x00},对应二进制为 "0110 0010, 1000 0000, 0000 0000";
解析数值个数uiElementNum为2;
数值[0]的值占4个bit,即 astElement[0].uiElementLength = 4;
数值[1]的值占5个bit,即 astElement[1].uiElementLength = 5;

输出描述

1
2
数值[0]的值为6,二进制为 "0110",即 astElement[0].uiElementValue = 6;
数值[1]的值为5,二进制为 "0010 1",即 astElement[1].uiElementValue = 5;

示例 1

输入

1
2
3
4
5
3
0x62 0x80 0x00
2
4
5

输出

1
2
6
5

解决方案

这道题目主要是对数字进行进制转换,输入为一个十六进制字符串,然后需要转换成对应的二进制字符串,我的做法是将字符串转换成十进制整数 (使用stoi函数),然后将这个十进制整数转化成二进制字符串,将转换后的二进制字符串保持到数组aInputByte,这里我不固定数组aInputByte大小,根据输入的参数uiInputLen动态设定数组大小,数据转换完毕后,调用Decode函数进行解析。

PS: 十六进制字符串转二进制字符串应该可以直接用映射关系来转换,因为每个十六进制字符对应的二进制是固定的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#include <iostream>
#include <string>
using namespace std;

struct ELEMENT_STRU {
unsigned int uiElementLength;
unsigned int uiElementValue;
};

void Decode(unsigned int uiInputLen, unsigned char aInputByte[],
unsigned int uiElementNum, ELEMENT_STRU astElement[])
{
unsigned int uiElementValue, uiElementLength, pow2;
int start, end;
start = 0;
for (int i=0; i<uiElementNum; i++) {
uiElementLength = astElement[i].uiElementLength;
uiElementValue = 0;
pow2 = 1;
end = start + uiElementLength;
for (int j=end-1; j>=start; j--) {
if (aInputByte[j] == '1')
uiElementValue = uiElementValue + pow2;
pow2 = 2 * pow2;
}
astElement[i].uiElementValue = uiElementValue;
start = end;
}
}

int main()
{
string str;
unsigned int uiInputLen, num, uiElementNum,flag;
unsigned int numberBytes;
int start, end;
unsigned char *aInputByte = NULL;
ELEMENT_STRU *astElement = NULL;
while (cin >> uiInputLen) {
aInputByte = (unsigned char *)malloc(sizeof(unsigned char) * 32 * uiInputLen);
start = 0;
end = 0;
for (int i=1; i<=uiInputLen; i++) {
cin >> str;
if (str.size() == 3) // 0xX
numberBytes = 4;
else if (str.size() == 4) // 0xXX
numberBytes = 8;
else if (str.size() == 5) // 0xXXX
numberBytes = 12;
else if (str.size() == 6) // 0xXXXX
numberBytes = 16;

end = start + numberBytes;

flag = 1;
num = stoi(str, 0, 16);
for (int j=end-1; j>=start; j--) {
if (flag & num)
aInputByte[j] = '1';
else
aInputByte[j] = '0';
flag = flag << 1;
}

start = end;
}

cin >> uiElementNum;
astElement = (ELEMENT_STRU*)malloc(sizeof(ELEMENT_STRU)*uiElementNum);
for (int i=0; i<uiElementNum; i++) {
cin >> num;
astElement[i].uiElementLength = num;
}

Decode(uiInputLen, aInputByte, uiElementNum, astElement);

for (int i=0; i<uiElementNum; i++)
cout << astElement[i].uiElementValue << endl;

free(aInputByte);
free(astElement);
}
return 0;
}

题目三 长整数相乘

问题描述

长整数相乘

输入描述

1
输入两个长整数,以空格隔开

输出描述

1
输出相乘后的结果

示例 1

输入

1
2
-12341234
43214321

输出

1
-533318047612114

解决方案

LeetCode有类似大整数相乘题目 43. Multiply Strings,和本题区别在于,LeetCode那题假设两个整数都是非负的,这题的整数可以是负数,在LeetCode 43题解题基础上加个符号判断即可解出该题。

问题描述

Write a function to delete a node (except the tail) in a singly linked list, given only access to that node.

Supposed the linked list is 1 -> 2 -> 3 -> 4 and you are given the third node with value 3, the linked list should become 1 -> 2 -> 4 after calling your function.

题目的意思是给单链表的节点指针 (不包括尾节点),然后删除该节点。

解决方案

如果按照常规单链表删除节点的操作,首先要找到要删除节点的前一个节点,然后再删除节点,但是这里只给定了待删除节点的指针,因此按照常规删除方法是不可行的,虽然待删除节点的前一节点我们不可知,但是我们可以知道该节点的下一个节点,如果我们把下一节点的内容复制到需要删除的节点上覆盖原有的内容,再把下一节点删除,那是不是就相当于把当前需要删除的节点删除了?根据题目描述,题目提供的节点不包括尾节点,因此不用考虑下一节点是 NULL 的情况。按照上面思路,可以有如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/

class Solution
{
public:
/*
* the node is not the last node
*/
void deleteNode(ListNode* node)
{
if (node == NULL)
return;

ListNode *next_node = node->next;
node->val = next_node->val;
node->next = next_node->next;
delete next_node;
}
};

在介绍排序算法之前,先约定数组表示为 $A$,大小为 $N$,数组下标从 $0$ 开始,排序的数组是从小到大排列

插入排序

在数组是反序的情况下,时间复杂度为 $O(N^2)$,在数组已排序的情况下,时间复杂度为 $O(N)$

1
2
3
4
5
6
7
8
9
10
11
void insertion_sort(int *nums, int n)
{
int p, tmp;
for (int i=1; i<n; i++)
{
tmp = nums[i];
for (p=i; p > 0 && nums[p-1] > tmp; p--)
nums[p] = nums[p-1];
nums[p] = tmp;
}
}

归并排序

时间复杂度为 $O(NlogN)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void merge(int *nums, int *tmp, int left_start, int right_start, int right_end)
{
int left_pos, right_pos, pos;

left_pos = left_start;
right_pos = right_start;
pos = left_start;
while (left_pos < right_start && right_pos <= right_end) {
if (nums[left_pos] <= nums[right_pos])
tmp[pos++] = nums[left_pos++];
else
tmp[pos++] = nums[right_pos++];
}

while (left_pos < right_start) {
tmp[pos++] = nums[left_pos++];
}
while (right_pos <= right_end) {
tmp[pos++] = nums[right_pos++];
}

// copy array tmp back to array nums
for (int i=left_start; i<=right_end; i++)
nums[i] = tmp[i];
}

void msort(int *nums, int *tmp, int left, int right)
{
if (left < right)
{
int center = (left + right) / 2;
msort(nums, tmp, left, center);
msort(nums, tmp, center+1, right);
merge(nums, tmp, left, center+1, right);
}
}

void merge_sort(int *nums, int n)
{
int *tmp = new int[n];
msort(nums, tmp, 0, n-1);
delete[] tmp;
}

快速排序

平均时间复杂度为 $O(NlogN)$,最坏的情形下,时间复杂度为 $O(N^2)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
int partition(int *nums, int left, int right)
{
int i = left - 1;
int tmp;
int pivot = nums[right];

for (int j=left; j<=right-1; j++) {
if (nums[j] <= pivot) {
i = i + 1;

tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}

i = i + 1;
tmp = nums[i];
nums[i] = pivot;
nums[right] = tmp;

return i;
}

void qsort(int *nums, int left, int right)
{
if (left < right) {
int p = partition(nums, left, right);
qsort(nums, left, p-1);
qsort(nums, p+1, right);
}
}

void quick_sort(int *nums, int n)
{
qsort(nums, 0, n-1);
}

问题描述:

Given an array of integers, return indices of the two numbers such that they add up to a specific target.

You may assume that each input would have exactly one solution, and you may not use the same element twice.

Example:

Given nums = [2, 7, 11, 15], target = 9. Because nums[0] + nums[1] = 2 + 7 = 9, return [0, 1].

时间复杂度为 O(n) 解法

首先分析下问题,根据问题描述,可以得知数组是无序的。如果用暴力方法解决问题,也就是先在数组中固定一个数字,再判断数组中其余的 $n-1$ 个数字与它的和是不是等于目标值,这样时间复杂度是 $O(n^2)$。这里关键地方就是在于固定一个数字时,如何快速在数组中寻找与它的和等于目标值,而不是通过遍历方法一个个找,这样时间复杂度要 $O(n)$,想到快速查找,第一时间可以想到的是哈希表 (Hash Map),因为这个查找元素的时间复杂度为 $O(1)$。

对于哈希表,我们可以用数组中的数字作为哈希表的key,用数字的下标作为哈希表的value。当我们从头遍历数组时,对于访问到的数字,我们首先判断哈希表中是否存在与它的和等于目标值的key,如果不存在,插入该数字到哈希表中,如果存在,算法返回两个数字对应的数组的下标。可以看出,这个算法只需要遍历一次数组,因此算法复杂度为 $O(n)$。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <unordered_map>
#include <iostream>
#include <vector>
using namespace std;

class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> nums_map;
unordered_map<int, int>::const_iterator iter;
vector<int> indices;
for (int i=0; i<nums.size(); i++) {
if ((iter = nums_map.find(target - nums[i])) == nums_map.end()) {
nums_map.insert(make_pair(nums[i], i));
} else {
indices.push_back(iter->second);
indices.push_back(i);
break;
}
}

return indices;
}
};

int main()
{
int target = 9;
vector<int> nums;
nums.push_back(2);
nums.push_back(7);
nums.push_back(11);
nums.push_back(15);

Solution sol;
vector<int> indices = sol.twoSum(nums, target);
if (indices.size() == 0) {
cout << "Not found" << endl;
} else {
cout << "Find: [" << indices[0] << ", " << indices[1] << "]" << endl;
}
return 0;
}

问题描述:

Given an array of integers, every element appears twice except for one. Find that single one.

Note:
Your algorithm should have a linear runtime complexity. Could you implement it without using extra memory?

时间复杂度为 O(n) 算法

算法要用到异或运算,异或运算 的规律如下所示:

0 xor 0 = 0

0 xor 1 = 1

1 xor 0 = 1

1 xor 1 = 0

很容易验证一个异或运算的性质:任何一个数字异或它自己都等于0,试想有个数组 $[A, B, A, C, B]$,数字 $A$ 和数字 $B$ 重复出现两次,数字 $C$ 只出现一次,如果我们将数组所有数字进行异或运算,会出现什么情况?首先,可以写出一个表达式
$$
A \text{ xor } B \text{ xor } A \text{ xor } C \text{ xor B}
$$

由于异或运算满足交换律和结合律,因此可以得到下面这个表达式
$$
(A \text{ xor } A ) \text{ xor } ( B \text{ xor } B) \text{ xor C}
$$
由于 $A \text{ xor } A = 0$ 以及 $B \text{ xor } B = 0$,并且0与任何数异或都为原来的数,因此上面的表达式最终化简为
$$
(A \text{ xor } A ) \text{ xor } ( B \text{ xor } B) \text{ xor C} = C
$$
可见,对数组所有数进行异或运算,最终结果就是那个只出现一次的数。知道算法原理,我们可以很容易写出实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <vector>
using namespace std;

class Solution {
public:
int singleNumber(vector<int>& nums) {
int ret = 0;
for (auto it=nums.begin(); it!=nums.end(); ++it)
ret ^= *it;

return ret;
}
};

int main()
{
Solution solu;

vector<int> v1 = {2, 3, 2, -1, 3};
cout << "Single Number is: " << solu.singleNumber(v1) << endl;

return 0;
}

问题描述:

Given two sorted integer arrays nums1 and nums2, merge nums2 into nums1 as one sorted array.

Note:
You may assume that nums1 has enough space (size that is greater or equal to m + n) to hold additional elements from nums2. The number of elements initialized in nums1 and nums2 are m and n respectively.

题目中说数组nums1大小为 m,数组nums2的大小为 n,要将数组nums2中的元素全部放到数组nums1中且保证数组nums1有序(这里有序假设是从小到大)。

时间复杂度为 O(nm) 的解法

初看到这个题目,一个最直观的想法就是设定两个指针分别指向两个数组开始的元素(令指针p1指向数组nums1,p2指向数组p2),然后从头开始遍历两个数组。

  • 如果指针p1指向的元素小于等于指针p2指向的元素,指针p1加一。
  • 如果指针p1指向的元素大于指针p2指向的元素,将指针p2指向的元素插入到指针p1指向的位置,指针p1指向的位置后面的元素都后移一位,指针p1和指针p2都加一。

这样操作直到数组nums1或数组nums2的元素遍历完,如果是数组nums1的元素先被遍历完,则将数组nums2剩下的元素都复制到数组nums1中,如果是数组nums2的元素先被遍历完,算法停止。在最坏的情况下,数组nums2的元素都小于数组nums1的元素,每次插入数组nums2的元素,都要移动数组nums1的 $O(m)$ 个字符,那么算法复杂度为 $O(nm)$。

时间复杂度为 O(n + m) 的解法

换个思路,我们不从头开始遍历数组,而是从数组后面开始遍历数组。这里我们准备三个指针:p、p1和p2,指针p指向数组nums1的第 n + m 个元素,指针p1指向数组nums1最后一个元素,指针p2指向数组nums2的最后一个元素。

  • 如果指针p1指向的元素小于指针p2指向的元素,将指针p2指向的元素复制到指针p指向的位置,指针p和p2减一。
  • 如果指针p1指向的元素大于等于指针p2指向的元素,将指针p1指向的元素复制到指针p指向的位置,指针p和p1减一。

这样操作直到数组nums1或数组nums2被遍历完,如果是数组nums1先被遍历完,将数组nums2剩余的元素复制到数组nums1中,如果是数组nums2先被遍历完,则算法停止。从上面分析我们可以看出,只需要遍历一次数组nums1和数组nums2就行,不需要移动元素,因此这个算法的时间复杂度为 $O(n + m)$。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <iostream>
#include <vector>
using namespace std;

class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {

if (m <= 0)
nums1 = nums2;
if (n <= 0)
return;

int p, p1, p2;
p1 = m - 1;
p2 = n - 1;
p = m + n - 1;

while (p >= 0 && p1 >= 0 && p2 >= 0) {
if (nums1[p1] >= nums2[p2]) {
nums1[p] = nums1[p1];
p1--;
} else if (nums1[p1] < nums2[p2]) {
nums1[p] = nums2[p2];
p2--;
}
p--;
}

if (p2 >= 0) {
while (p >=0 && p2 >=0)
nums1[p--] = nums2[p2--];
}
}
};

int main()
{
Solution solu;

vector<int> nums1 = {1, 3, 5, 7, 0, 0, 0};
vector<int> nums2 = {2, 4, 6};
solu.merge(nums1, 4, nums2, nums2.size());

for (auto it=nums1.begin(); it!=nums1.end(); ++it)
cout << *it << " ";
cout << endl;

vector<int> nums3 = {0, 0, 3, 0, 0, 0, 0, 0, 0};
vector<int> nums4 = {-1, 1, 1, 1, 2, 3};
solu.merge(nums3, 3, nums4, nums4.size());

for (auto it=nums3.begin(); it!=nums3.end(); ++it)
cout << *it << " ";
cout << endl;

return 0;
}

问题描述:

Find the kth largest element in an unsorted array. Note that it is the kth largest element in the sorted order, not the kth distinct element.
For example,
Given [3,2,1,5,6,4] and k = 2, return 5.

Note:
You may assume k is always valid, 1 ≤ k ≤ array’s length.

这道题目最简单的思路莫过于把输入的 $n$ 个数进行排序,排序之后第 $k$ 个数字 (如果数是从大到小排序) 就所要求的第 $k$ 大的数字,这种思路的时间复杂度是 $O(nlog(n))$ 。但是这样还不是最快的做法,接下来讨论更快的算法。

时间复杂度为 O(nlog(k)) 的算法

我们可以先创建一个大小为 $k$ 的数据容器来存储最大的 $k$ 个数,接下来每次从输入的 $n$ 个数中读入一个数。如果容器中已有的数字少于 $k$ 个,则直接把这次读入的数放入容器之中;如果容器中已有 $k$ 个数字了,也就是容器已经满了,此时只能替换容器中的数字了。找出容器中 $k$ 个数的最小值,然后拿这次待插入的数与其比较,如果待插入的数字比容器中的最小值要大,则用这个数替换当前已有的最小值,如果待插入的数字比这个最小值要小,那么这个数不能是最大的 $k$ 个数之一,则抛弃这个数字。

因此,当容器满了以后,我们要做三件事:一是在 $k$ 个数中找到最小数;二是有可能在这个容器中删除最小数; 三是有可能要插入一个新的数字。如果使用一棵二叉树来实现这个容器,那么我们能在 $O(log(k))$ 的时间内实现这三步操作。因此,对于 $n$ 个输入数字而言,总的时间效率是 $O(n log(k))$。

由于每次都需要找到 $k$ 个数中的最小值,很自然可以想到用最小堆。当然,我们还可以考虑是用红黑树来实现这个容器,红黑树可以保证查找、删除和插入这些操作都只需要 $O(log(k))$ 时间,在STL中,set 和 multiset 都是基于红黑树实现。

基于最小堆实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

#include <queue>
#include <iostream>
using namespace std;

class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int, vector<int>, greater<int>> k_numbers;
int min_number;

for (auto it=nums.begin(); it!=nums.end(); ++it) {
if (k_numbers.size() < k) {
k_numbers.push(*it);
} else {
min_number = k_numbers.top();
if (*it > min_number) {
k_numbers.pop();
k_numbers.push(*it);
}
}
}

return k_numbers.top();
}
};

int main()
{
Solution solu;
vector<int> nums = {3, 2, 1, 5, 6, 4};

cout << "The 2th max number is " << solu.findKthLargest(nums, 2) << endl;

return 0;
}

基于红黑树实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

#include <vector>
#include <set>
#include <iostream>
using namespace std;

class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
multiset<int> k_numbers;
multiset<int>::iterator set_iterator;

for (auto it=nums.begin(); it!=nums.end(); ++it) {
if (k_numbers.size() < k) {
k_numbers.insert(*it);
} else {
set_iterator = k_numbers.begin();
if (*it > *set_iterator) {
k_numbers.erase(set_iterator);
k_numbers.insert(*it);
}
}
}

return *(k_numbers.begin());
}
};

int main()
{
Solution solu;
vector<int> nums = {3, 2, 1, 5, 6, 4};

cout << "The 2th max number is " << solu.findKthLargest(nums, 2) << endl;

return 0;
}