SudokuSolver 2.0 实现效果

H:\Read\num\Release>sudoku.exe

Order please:

Sudoku Solver 2.0 2021/10/2 by readalps

Order List:
load-quiz <file>: load quiz from file
show: show quiz info
step: step forward
run: run till the end or a new solution met
bye: quit Order please:

可以看到,SudokuSolver 2.0 版和之前的 1.0 版用法上完全一样。2.0 版主要实现了一些精确求解的方法,减少求解过程中的猜测,提升求解速度。仍以 一道心形数独题的求解 里的那道题为例,来具体看一下:

Order please:
step
000 000 000
023 000 780
100 406 009 400 050 001
900 000 006
060 000 090 005 000 800
000 301 000
000 090 000 Steps:1
Candidates:
[1,1]: 5 6 7 8 [1,2]: 4 5 7 8 9 [1,3]: 4 6 7 8 9
...
[2,1]: 5 6 [2,4]: 1 5 9 [2,5]: 1
[2,6]: 5 9 [2,9]: 4 5 [3,2]: 5 7 8
...
[9,9]: 2 3 4 5 7
The foremost cell with one candidate at [2,5] Order please:

step 命令的输出,对比 1.0 版,2.0 版增加了 steps 累计值输出;待填格的候选值调整为三格共一行;若存在单候选值的 cell,则会在末尾部分输出最靠前的单候选值的 cell 位置。

从上面输出的信息可以看到,第一步只是求各个待填格的候选值,而没有做具体的填值操作,这和 1.0 版是一样的。

Order please:
step
000 000 000
023 010 780
100 406 009 400 050 001
900 000 006
060 000 090 005 000 800
000 301 000
000 090 000 Steps:2
Candidates:
[1,1]: 5 6 7 8 [1,2]: 4 5 7 8 9 [1,3]: 4 6 7 8 9
[1,4]: 2 5 7 8 9 [1,5]: 2 3 7 8 [1,6]: 2 3 5 7 8 9
[1,7]: 1 2 3 4 5 6 [1,8]: 1 2 3 4 5 6 [1,9]: 2 3 4 5
[2,1]: 5 6 [2,4]: 5 9 [2,6]: 5 9
[2,9]: 4 5 [3,2]: 5 7 8 [3,3]: 7 8
...
[9,7]: 1 2 3 4 5 6 [9,8]: 1 2 3 4 5 6 7 [9,9]: 2 3 4 5 7 Order please:

第二步在 [2,5] 位置填上了 1,并对所有待填格的候选值做出调整,这和 1.0 版依然是一样的。

所有待填格的候选值数目都大于 1,最靠前的最小候选值数目的待填格为 [2,1],1.0 版下一步就开始第一级猜测:[2,1] = 5。现在来看 2.0 版有什么不同:

Order please:
step
row 2 shrunken by group
000 000 000
623 010 784
100 406 009 400 050 001
900 000 006
060 000 090 005 000 800
000 301 000
000 090 000 Steps:5
Candidates:
[1,1]: 5 7 8 [1,2]: 4 5 7 8 9 [1,3]: 4 7 8 9
[1,4]: 2 5 7 8 9 [1,5]: 2 3 7 8 [1,6]: 2 3 5 7 8 9
[1,7]: 1 2 3 5 6 [1,8]: 1 2 3 5 6 [1,9]: 2 3 5
[2,4]: 5 9 [2,6]: 5 9 [3,2]: 5 7 8
...
[9,9]: 2 3 5 7 Order please:

可以看到 [2,1] 位置上直接填上了 6,跳过了 [2,1] = 5 的猜测,同时还有 [2,9] = 4。相关的一条信息为:

row 2 shrunken by group

这是说,第 2 行所在的 9 个 cell 为一组,单纯由这一组 cell 的候选值分布情况就可以推导出某个(些)cell 的唯一取值来。具体看一下这里的情形,前两步之后,第 2 行有 4 个待填格(0-cell),其候选值分布情况为:

[2,1]: 5 6    [2,4]: 5 9    [2,6]: 5 9
[2,9]: 4 5

6 只在 [2,1] 的候选值中出现,所以必然有 [2,1] = 6;同样地,有 [2,9] = 4。

这便是第三步的行为,它把 [2,1] 和 [2,9] 的候选值个数都缩减为 1,于是第四步接着做填值和候选值调整的工作。即交互输入的第 3 次 step 命令在程序内部实际走了两步,所以有 Steps:4 的输出信息。

往下不再单步走了,直接用一个 run 命令跑完:

Order please:
run
row 4 shrunken by group
row 4 shrunken by group
row 7 shrunken by group
row 7 shrunken by group
row 7 shrunken by group
row 7 shrunken by group
row 8 shrunken by group
row 8 shrunken by group
row 8 shrunken by group
row 8 shrunken by group
row 1 shrunken by group
row 6 shrunken by group
row 3 shrunken by group
row 5 shrunken by group
row 6 shrunken by group
row 5 shrunken by group
row 9 shrunken by group
row 9 shrunken by group
col 2 shrunken by group
549 738 162
623 915 784
187 426 359 438 659 271
951 872 436
762 143 598 395 264 817
276 381 945
814 597 623 Done [steps:58, solution sum:1].
Run time: 40 milliseconds; steps: 58, solution sum: 1. Order please:
step
No more solution (solution sum is 1).

从上面的输出信息可以看出,2.0 版求解这个心形数独题完全没有猜测。

SudokuSolver 2.0 程序实现

在 1.0 版的基础上,2.0 版做了以下方面的一些改进。

解决 load-quiz 命令实现的一处 bug

即输入交互命令 load-quiz H:\s.txt 会报无效参数(Invalid argument)的错误:

Order please:
load-quiz H:\s.txt
Fail to open quiz file H:\s.txt with err 22:Invalid argument.

问题出在 dealOrder 函数里如下标黄行的代码:

void dealOrder(std::string& strOrder)
{
std::string strEx;
if ("bye" == strOrder)
setQuit();
else if (matchPrefixEx(strOrder, "load-quiz", strEx))
CQuizDealer::instance()->loadQuiz(strEx);

调用 matchPrefixEx 函数传入的第二个参数为 "load-quiz",第一参数传入

load-quiz H:\s.txt

matchPrefixEx 会给输出参数 strEx 填上 " H:\s.txt",即开头多了一个空格字符,导致调用到 loadQuiz 接口里打开文件时报非法参数的错误。

因此,修改方法很简单,只需把标黄代码行里的 "load-quiz" 改为 "load-quiz " 即可。

新增 CQuizDealer::adjustCandidates 接口

新增的 adjustCandidates 接口替代原有的 reCalcCandidates 接口。1.0 版里 reCalcCandidates 接口(参见 SudokuSolver 1.0:用C++实现的数独解题程序 【二】),对 m_setBlank 里的每个 0-cell 都重新计算其候选值集合,而 adjustCandidates 只对新填值 cell 所在行、所在列、所在宫里 0-cell 的候选值集合做微调的收缩处理,即从集合中去掉新填值。具体实现如下:

 1 bool CQuizDealer::adjustCandidates(u8 idx, u8 val)
2 {
3 u8 row = idx / 9;
4 u8 col = idx % 9;
5 u8 blk = (row / 3) * 3 + (col / 3);
6 u8 rowBase = row * 9;
7 for (u8 colIdx = 0; colIdx < 9; ++colIdx) {
8 Cell& cel = m_seqCell[rowBase + colIdx];
9 if (cel.val != 0)
10 continue;
11 if (!removeVal(cel.candidates, val)) {
12 printf("shrinking %d from [%d,%d] went wrong\n", (int)val, (int)row + 1, (int)colIdx + 1);
13 return false;
14 }
15 }
16 for (u8 rowIdx = 0; rowIdx < 81; rowIdx += 9) {
17 Cell& cel = m_seqCell[rowIdx + col];
18 if (cel.val != 0)
19 continue;
20 if (!removeVal(cel.candidates, val)) {
21 printf("shrinking %d from [%d,%d] went wrong\n", (int)val, (int)rowIdx / 9 + 1, (int)col + 1);
22 return false;
23 }
24 }
25 for (u8 blkIdx = 0; blkIdx < 9; ++blkIdx) {
26 Cell& cel = m_seqCell[block2row(blk, blkIdx) * 9 + block2col(blk, blkIdx)];
27 if (cel.val != 0)
28 continue;
29 if (!removeVal(cel.candidates, val)) {
30 printf("shrinking %d from blk[%d,%d] went wrong\n", (int)val, (int)blk + 1, (int)blkIdx + 1);
31 return false;
32 }
33 }
34 return true;
35 }

其中用到的 removeVal 函数实现如下:

1 bool removeVal(u8* pVals, u8 val)
2 {
3 shrink(pVals, val);
4 return (pVals[0] != 0);
5 }

shrink 为候选值集合收缩函数,实现如下:

 1 void shrink(u8* pVals, u8 val)
2 {
3 for (u8 idx = 1; idx <= pVals[0]; ++idx) {
4 if (pVals[idx] != val)
5 continue;
6 for (u8 pos = idx + 1; pos <= pVals[0]; ++pos)
7 pVals[pos - 1] = pVals[pos];
8 pVals[0] = pVals[0] - 1;
9 break;
10 }
11 }

shift 接口实现只改动如下所示的末尾一行代码:

 1 bool CQuizDealer::shift(u8 idx, u8 valIdx)
2 {
3 m_seqCell[idx].val = m_seqCell[idx].candidates[valIdx];
4 m_seqCell[idx].candidates[0] = 0;
5 if (!adjustTakens(idx, m_seqCell[idx].val))
6 return false;
7 std::set<u8>::iterator it = m_setBlank.find(idx);
8 m_setBlank.erase(it);
9 return adjustCandidates(idx, m_seqCell[idx].val);
10 }

一些精确求解方法的实现

这部分增加的代码量最多,也使得 2.0 版的代码总量比 1.0 版的代码总量翻了一倍。为避免重复,以下代码内容和 1.0 版相同的部分多使用省略号表示。

CQuizDealer 声明部分增加的内容

class CQuizDealer
{
public:
...
private:
enum {STA_UNLOADED = 0, STA_LOADED, STA_INVALID, STA_VALID, STA_DONE};
enum {RET_PENDING, RET_OK, RET_WRONG};
...
bool calcCandidates(u8 idx);
bool adjustCandidates(u8 idx, u8 val);
void guess(u8 idx);
...
void useSnapshot(Snapshot* pSnap); u8 filterCandidates();
u8 filterRowGroup(u8 row);
u8 filterColGroup(u8 col);
u8 filterBlkGroup(u8 blk);
u8 filterRowCandidatesEx(u8 row);
bool filterOneGroup(u8* pGrp);
u8 filterRowByPolicy1(u8 row, u8 idx, u8* pVals, u8* pBlkTakens);
u8 shrinkRowCandidatesP1(u8* pBlk, u8* pRow, u8 zeroSum, u8 row, u8 colBase);
u8 shrinkCandidates(u8 cellPos, u8* pVals);
u8 filterRowByPolicy2(u8 row, u8 idx, u8* pVals, u8* pBlkTakens);
u8 shrinkRowCandidatesP2(u8* pBlk, u8* pRow, u8 zeroSum, u8 row, u8 colBase);
void calcExCols(u8* pEx, u8* pBlk, u8* pRow, u8 rowBase, u8 colBase, bool ply1);
void setBlkRowTakens(u8 blkBase, u8 innRow, u8* pTakens);
void setRowTakensInBlk(u8 rowBase, u8 colBase, u8* pVals);
u8 filterColCandidatesEx(u8 col);
void setBlkColTakens(u8 blkBase, u8 innCol, u8* pTakens);
void setColTakensInBlk(u8 col, u8 rowBase, u8* pVals);
u8 filterColByPolicy1(u8 col, u8 idx, u8* pVals, u8* pBlkTakens);
u8 shrinkColCandidatesP1(u8* pBlk, u8* pCol, u8 zeroSum, u8 col, u8 segRowBase);
u8 filterColByPolicy2(u8 col, u8 idx, u8* pVals, u8* pBlkTakens);
u8 shrinkColCandidatesP2(u8* pBlk, u8* pCol, u8 zeroSum, u8 col, u8 segRowBase);
void calcExRows(u8* pEx, u8* pBlk, u8* pCol, u8 col, u8 segRowBase, bool ply1); Cell m_seqCell[81];
...
};

adjust 接口实现修改

 1 void CQuizDealer::adjust()
2 {
3 if (m_state != STA_VALID)
4 return;
5 bool changed = false;
6 u8 guessIdx = 0;
7 u8 lowestSum = 10;
8 for (std::set<u8>::iterator it = m_setBlank.begin(); it != m_setBlank.end();) {
9 u8 idx = *it;
10 u8 sum = m_seqCell[idx].candidates[0];
11 if (sum != 1) {
12 if (sum < lowestSum) {
13 lowestSum = sum;
14 guessIdx = idx;
15 }
16 ++it;
17 continue;
18 }
19 m_seqCell[idx].val = m_seqCell[idx].candidates[1];
20 m_seqCell[idx].candidates[0] = 0;
21 if (!adjustTakens(idx, m_seqCell[idx].val)) {
22 nextGuess();
23 return;
24 }
25 m_setBlank.erase(it++);
26 if (!adjustCandidates(idx, m_seqCell[idx].val)) {
27 nextGuess();
28 return;
29 }
30 changed = true;
31 }
32 if (changed)
33 ++m_steps;
34 if (m_setBlank.empty()) {
35 m_state = STA_DONE;
36 m_soluSum++;
37 return;
38 }
39 if (!changed) {
40 u8 v = filterCandidates();
41 if (v == RET_OK)
42 adjust();
43 else if (v == RET_WRONG)
44 nextGuess();
45 else
46 guess(guessIdx);
47 return;
48 }
49 }

可以看到,adjust 接口的修改主要是两处:一处(lines 26-29)是每当给一个 0-cell 填值后随即调用 adjustCandidates 接口做相关 cells 的候选值集合收缩处理;另一处(lines 39-48)是当前所有的 0-cell 都没有收缩为单候选值时,调用新增接口 filterCandidates 对 0-cell 做进一步的候选值集合收缩处理,并根据收缩结果的不同调用相应的接口。

filterCandidates 接口有三种返回值:RET_PENDING、RET_OK、RET_WRONG,分别对应:所有 0-cell 的候选值数目都大于 1 的情形、某 0-cell 的候选值数目等于 1 的情形、某 0-cell 的候选值数目等于 0 的情形(即无法填值的情形)。

新增的 filterCandidates 接口

 1 u8 CQuizDealer::filterCandidates()
2 {
3 ++m_steps;
4 u8 ret = RET_PENDING;
5 for (u8 row = 0; row < 9; ++row)
6 if (ret =filterRowGroup(row))
7 return ret;
8 for (u8 col = 0; col < 9; ++col)
9 if (ret =filterColGroup(col))
10 return ret;
11 for (u8 blk = 0; blk < 9; ++blk)
12 if (ret =filterBlkGroup(blk))
13 return ret;
14 for (u8 row = 0; row < 9; ++row)
15 if (ret =filterRowCandidatesEx(row))
16 return ret;
17 for (u8 col = 0; col < 9; ++col)
18 if (ret =filterColCandidatesEx(col))
19 return ret;
20 return ret;
21 }

filterCandidates 接口实现内部汇集了两类候选值集合收缩算法,如上梅色标注的子接口属于第一类,浅天蓝色标注的子接口属于第二类。

filterRowGroup 接口

 1 u8 CQuizDealer::filterRowGroup(u8 row)
2 {
3 u8 celIdxs[10] = {0}; // first item denotes sum of zeros
4 u8 base = row * 9;
5 for (u8 col = 0; col < 9; ++col) {
6 if (m_seqCell[base + col].val == 0) {
7 celIdxs[0] += 1;
8 celIdxs[celIdxs[0]] = base + col;
9 }
10 }
11 if (celIdxs[0] == 0)
12 return RET_PENDING;
13 if (filterOneGroup(celIdxs)) {
14 printf("row %d shrunken by group\n", (int)row + 1);
15 return RET_OK;
16 }
17 return RET_PENDING;
18 }

fillRowGroup 接口实现的候选值集合收缩算法在前面的实例中已经做过陈述。具体从实现看,代码分为两部分:

(1)遍历由参数 row 指定的行,找出该行中的全体 0-cell,并把它们的下标记录到数组 celIdxs 中,该行 0-cell 的总数记录在 celIdxs 的头项里;

(2)调用 filterOneGroup(celIdxs) 实施对指定一组 cell 做候选值集合收缩处理。

filterColGroup 和 filterBlkGroup 接口

 1 u8 CQuizDealer::filterColGroup(u8 col)
2 {
3 u8 celIdxs[10] = {0}; // first item denotes sum of zeros
4 for (u8 row = 0; row < 9; ++row) {
5 u8 celIdx = row * 9 + col;
6 if (m_seqCell[celIdx].val == 0) {
7 celIdxs[0] += 1;
8 celIdxs[celIdxs[0]] = celIdx;
9 }
10 }
11 if (celIdxs[0] == 0)
12 return RET_PENDING;
13 if (filterOneGroup(celIdxs)) {
14 printf("col %d shrunken by group\n", (int)col + 1);
15 return RET_OK;
16 }
17 return RET_PENDING;
18 }
19
20 u8 CQuizDealer::filterBlkGroup(u8 blk)
21 {
22 u8 celIdxs[10] = {0}; // first item denotes sum of zeros
23 for (u8 idx = 0; idx < 9; ++idx) {
24 u8 celIdx = block2row(blk, idx) * 9 + block2col(blk, idx);
25 if (m_seqCell[celIdx].val == 0) {
26 celIdxs[0] += 1;
27 celIdxs[celIdxs[0]] = celIdx;
28 }
29 }
30 if (celIdxs[0] == 0)
31 return RET_PENDING;
32 if (filterOneGroup(celIdxs)) {
33 printf("blk %d shrunken by group\n", (int)blk + 1);
34 return RET_OK;
35 }
36 return RET_PENDING;
37 }

这两个接口实现和 filterRowGroup 的实现是类似的,只是以一列 cell 或 一宫 cell 为一组。三个接口的核心算法实现都在 filterOneGroup 接口。

filterOneGroup 接口

 1 bool CQuizDealer::filterOneGroup(u8* pGrp)
2 {
3 u8 size = pGrp[0];
4 u8 times[20] = {0};
5 for (u8 idx = 1; idx <= size; ++idx) {
6 u8 sum = m_seqCell[pGrp[idx]].candidates[0];
7 for (u8 inn = 1; inn <= sum; ++inn) {
8 u8 val = m_seqCell[pGrp[idx]].candidates[inn];
9 times[val] += 1;
10 times[val + 9] = pGrp[idx];
11 }
12 }
13 bool ret = false;
14 for (u8 val = 1; val <= 9; ++val) {
15 if (times[val] == 1) {
16 ret = true;
17 u8 celIdx = times[val + 9];
18 m_seqCell[celIdx].candidates[0] = 1;
19 m_seqCell[celIdx].candidates[1] = val;
20 }
21 }
22 return ret;
23 }

该接口的算法实现很简单。用了一个 times 数组用于累计指定一组 cell 的各个候选值出现的次数,比如某个 0-cell 有候选值 3 和 8,那么 times[3] 和 times[8] 最终会分别累计出 3 和 8 各自出现的次数,同时 times[3 + 9] 和 times[8 + 9] 里会分别记录最后出现 3 和 8 的那个 0-cell 的下标。这样,当 遍历完指定一组的 0-cell,检查 times 数组中下标 1 到 9 的单元有取值为 1 的,就说明对应的 0-cell 可以收缩为单个候选值。

第二类候选值集合收缩算法实现代码放到下一篇。

SudokuSolver 2.0:用C++实现的数独解题程序 【一】的更多相关文章

  1. SudokuSolver 1.0:用C++实现的数独解题程序 【二】

    本篇是 SudokuSolver 1.0:用C++实现的数独解题程序 [一] 的续篇. CQuizDealer::loadQuiz 接口实现 1 CQuizDealer* CQuizDealer::s ...

  2. 用C++实现的数独解题程序 SudokuSolver 2.3 及实例分析

    SudokuSolver 2.3 程序实现 用C++实现的数独解题程序 SudokuSolver 2.2 及实例分析 里新发现了一处可以改进 grp 算法的地方,本次版本实现了对应的改进 grp 算法 ...

  3. 用C++实现的数独解题程序 SudokuSolver 2.2 及实例分析

    SudokuSolver 2.2 程序实现 根据 用C++实现的数独解题程序 SudokuSolver 2.1 及实例分析 里分析,对 2.1 版做了一些改进和尝试. CQuizDealer 类声明部 ...

  4. 用C++实现的数独解题程序 SudokuSolver 2.1 及实例分析

    SudokuSolver 2.1 程序实现 在 2.0 版的基础上,2.1 版在输出信息上做了一些改进,并增加了 runtil <steps> 命令,方便做实例分析. CQuizDeale ...

  5. 用C++实现的数独解题程序 SudokuSolver 2.4 及实例分析

    SudokuSolver 2.4 程序实现 本次版本实现了 用C++实现的数独解题程序 SudokuSolver 2.3 及实例分析 里发现的第三个不完全收缩 grp 算法 thirdGreenWor ...

  6. 用C++实现的数独解题程序 SudokuSolver 2.6 的新功能及相关分析

    SudokuSolver 2.6 的新功能及相关分析 SudokuSolver 2.6 的命令清单如下: H:\Read\num\Release>sudoku.exe Order please: ...

  7. SudokuSolver 1.0:用C++实现的数独解题程序 【一】

    SudokuSolver 1.0 用法与实现效果 SudokuSolver 是一个提供命令交互的命令行程序,提供的命令清单有: H:\Read\num\Release>sudoku.exe Or ...

  8. 用C++实现的数独解题程序 SudokuSolver 2.7 及实例分析

    引言:一个 bug 的发现 在 MobaXterm 上看到有内置的 Sudoku 游戏,于是拿 SudokuSolver 求解,随机出题,一上来是个 medium 级别的题: 073 000 060 ...

  9. 数独GUI程序项目实现

    数独GUI程序项目实现 导语:最近玩上了数独这个游戏,但是找到的几个PC端数独游戏都有点老了...我就想自己做一个数独小游戏,也是一个不错的选择. 前期我在网上简单地查看了一些数独游戏的界面,代码.好 ...

随机推荐

  1. 移动端常用单位——rem

    移动端常用单位: ①px:像素大小,固定值 ②%:百分比 ③em(不常用,但是在首行缩进时可以使用):相对自身的font大小(当自身的字体大小也是em做单位时,才会以父元素的字体大小为基准单位) ④r ...

  2. PowerDotNet平台化软件架构设计与实现系列(01):基础数据平台

    本系列我将主要通过图片和少许文字讲解通过个人自研的PowerDotNet进行快速开发平台化软件产品. PowerDotNet不仅仅是包含像Newtonsoft.Json.Dapper.Quartz.R ...

  3. 使用 IDEA 配合 Dockerfile 部署 SpringBoot 工程

    准备 SpringBoot 工程 新建 SpringBoot 项目,默认的端口是 8080 ,新建 Controller 和 Mapping @RestController public class ...

  4. 这款打怪升级的小游戏,7 年前出生于 GitHub 社区,如今在谷歌商店有 8 万人打了满分

    今天我在 GitHub 摸鱼寻找新的"目标"时,发现了一个开源项目是 RougeLike 类的角色扮演游戏「破碎版像素地牢」(Shattered Pixel Dungeon)类似魔 ...

  5. TDSQL(MySQL版)之DB组件升级

    随着数据库产品的更新迭代,修复bug等等,产品避免不了会出现升级的需求.TDSQL(MysqL版)也会有这方面的需求.接下来我就说说如何对现有TDSQL(MySQL版)集群组件进行升级,而不影响业务. ...

  6. Docker(40)- docker 实战三之安装 ES+Kibana

    背景 参考了狂神老师的 Docker 教程,非常棒! https://www.bilibili.com/video/BV1og4y1q7M4?p=16 es 前言 es 暴露的端口很多 es 十分耗内 ...

  7. session案例之验证码

    一.需求分析 其中,一张图片就是一个单独的请求: 一个验证验证码的Servlet,还有一个验证用户名和密码的Servlet,两次都可能有错误信息返回到前端页面,所以前面页面要从request域中获取返 ...

  8. 前端框架VUE——安装及初始化

    本篇文章适合,想要学习 vue,但对 vue 又没有接触过的同学阅读,是非常基础的内容.告诉大家使用 vue 时的安装方式,及如何创建实例,展示内容. 一.安装方式 vue 是一种前端框架,所以使用前 ...

  9. 【转】Linux 查看端口占用情况

    Linux 查看端口占用情况可以使用 lsof 和 netstat 命令. lsof lsof(list open files)是一个列出当前系统打开文件的工具. lsof 查看端口占用语法格式: l ...

  10. 【转】shell中的$0 $n $# $* $@ $? $$ 变量 if case for while

    shell中的$0 $n $# $* $@ $? $$  shell 编程 | shift 命令用法笔记 $0当前脚本的文件名 $n传递给脚本或函数的参数.n 是一个数字,表示第几个参数.例如,第一个 ...