使用java swing制作人机五子棋

  • 背景
  • 算法原理
    • 棋盘分值更新范围
    • 更新分值方法
    • 判断分值方法
    • 局势分数权重
  • 设计模式
    • 战局类Battle
    • AI类 Integration
  • UI设计
    • 窗口类UIinterface (继承JFrame)
    • 棋子层类(继承JPanel)
    • 棋子按钮类(继承JButton)
  • 总结
  • 项目下载

背景

算法老师要求交个大作业什么的,自己就选择了制作“利用所学算法知识设计一个人机对弈程序或软件”这个课题,顺便首次记录一下自己独自写一个小项目的过程,中间花费了不少心思,果然只有亲身经历才能深刻体会算法和设计模式的魅力,写下这篇文章也算对自己的努力有个交代。

算法原理

使用四种符号代表棋盘上各个点的状态:X代表黑棋(AI),O代表白棋(人类),L代表无人下棋,E代表越界(棋盘范围之外)。
想要AI下棋,就必须掌握当前局势,知道自己应该或更倾向走哪个位置。这里采用贪婪策略,计算AI走这个点的分值,每个点都算一次,最后选取分值最大的点,标记为X,即完成下棋,等待玩家下棋,重复循环。

棋盘分值更新范围

想要实现贪婪,就必须使得每个点的分值都可以获取到,然而,这并不代表每下一个棋子,都必须更新n*n的棋盘,这是因为每个棋子影响的范围是有限的,你下最左上角的一个棋子,再怎么样也不可能和最右下角的棋子凑成五连,或是阻碍右下角的棋子凑成五连,这里定义一个词语“米围”:以自己为中心,上面四个,下面四个,左边四个,右边四个,左上四个,左下四个,右上四个,右下四个,共二十八个位置,这就是一个点的影响范围,如图所示,白点是下的点,而红色区域是因为白点的存在而战局意义改变的点,而除红色区域外的点白点都无法阻止或凑成五连。

// 计算刚才下的点的"米围"点的分值public void calculateScore(int a, int b) {for (int i = a - 4; i <= a + 4; i++) {updatePoint(i, b, "|");//从上往下}for (int i = b - 4; i <= b + 4; i++) {updatePoint(a, i, "-");//从左往右}for (int i = a - 4, j = b - 4; i <= a + 4; i++, j++) {updatePoint(i, j, "\\");//从左上往右下}for (int i = a - 4, j = b + 4; i <= a + 4; i++, j--) {updatePoint(i, j, "/");//从右上往左下}}

更新分值方法

首先如果这个点已经有白棋或黑棋占领,即符号为X或O,则将其分值变为-1,就永远也不可能下这个点了。如果还是空位,就必须观察自己周围的局势,判断自己下这个点有何意义:是为了凑成五连,还是为了阻止五连?可以凑成四连进攻一手,还是堵个四连防守一下?
这个点的分值被四个方向的点影响着,正好也是“米围”的点,因为如果下了这个点,就可能让和“米围”的点凑成五连,因此就只需要观察“米围”的点就行了。此处可以拆成四处,即“——”,“|”,“\”,“/”四个方向。
这里由于计算分值总是被动的(只有每次玩家和AI下完棋后才会更新分值),因此计算分值时可以得知“刚下的点”在“要计算的点”的哪一个方向,这样就可以每次更新一个方向,只不过需要一个nn4的数组记录一下,当然也可以每次都计算四个方向(反正人反应不过来,还能节省内存),这里使用前者,空间换时间。

private void updatePoint(int a, int b, String string) {char c = battle.getBattle(a, b);if (c != 'L') {setPoint(a, b, -1);return;}String s = "";switch (string) {case "|":for (int i = a - 4; i <= a + 4; i++) {s += battle.getBattle(i, b);//从上往下}this.pointDetil[a][b][0] = getScore(s);//代表丨的局势break;case "-":for (int i = b - 4; i <= b + 4; i++) {s += battle.getBattle(a, i);//从左往右}this.pointDetil[a][b][1] = getScore(s);//代表——的局势break;case "\\":for (int i = a - 4, j = b - 4; i <= a + 4; i++, j++) {s += battle.getBattle(i, j);//从左上往右下}this.pointDetil[a][b][2] = getScore(s);//代表\的局势break;case "/":for (int i = a - 4, j = b + 4; i <= a + 4; i++, j--) {s += battle.getBattle(i, j);//从右上往左下}this.pointDetil[a][b][3] = getScore(s);//代表/的局势break;}this.point[a][b] = getSumPoint(a, b);//计算总分值return;}// 计算总的分值(横竖斜加起来)private int getSumPoint(int a, int b) {return this.pointDetil[a][b][0] + this.pointDetil[a][b][1] + this.pointDetil[a][b][2]+ this.pointDetil[a][b][3];}

判断分值方法

现在我们得到了代表局势的字符串,接下来就该考虑如何计算分值了,例如我们取得了“LXXXLOOLL”,由于“米围”的范围,所以这必是个长度为9的字符串,由于只有第四位为L时才能下棋,所以第四位必为L。
“LXXXLOOLL”这9个字符中,我们可以知道下这个点就可以凑成一个四连,并阻止白棋的二连,这是对己方有利的,因此就可以给特定的分值。
可以很轻易的用正则表达式实现:

// 计算分值protected int getScore1(String str) {int L_num = 0;int O_num = 0;int X_num = 0;for (int i = 0; i < str.length(); i++) {if (str.charAt(i) == 'L') {L_num++;} else if (str.charAt(i) == 'X') {X_num++;} else if (str.charAt(i) == 'O') {O_num++;}}String strR = new StringBuilder(str).reverse().toString();// 字符串反转if (X_num > 3) {if (str.matches("^..XXLXX.*") || str.matches("^.XXXLX.*") || strR.matches("^.XXXLX.*")|| str.matches("^XXXXL.*") || strR.matches("^XXXXL.*")) {return 40000;// 获胜分}}if (O_num > 3) {if (str.matches("^..OOLOO.*") || str.matches("^.OOOLO.*") || strR.matches("^.OOOLO.*")|| str.matches("^OOOOL.*") || strR.matches("^OOOOL.*")) {return 10000;// 救命分}}if (L_num > 2) {if (X_num >= O_num) // 优先进攻if (str.matches("^.XXXLL.*") || strR.matches("^.XXXLL.*")) {return 2500;// 进攻分} else if (str.matches("^..XXL[LX][LX].*") || strR.matches("^..XXL[LX][LX].*")) {return 1000;} else if (str.matches("^.[LX][LX][LX]LX.*") || strR.matches("^.[LX][LX][LX]LX.*")) {return 400;}} else {if (str.matches("^.OOOLL.*") || strR.matches("^.OOOLL.*")) {return 2100;// 防守分} else if (str.matches("^..OOL[LO][LO].*") || strR.matches("^..OOL[LO][LO].*")) {return 900;} else if (str.matches("^.[LO][LO][LO]LO.*") || strR.matches("^.[LO][LO][LO]LO.*")) {return 300;}}return 1;}

不过上述使用正则表达式的方法并非明智之举,因为它匹配时间长(时间复杂度大约为平方级n2,此处n为9,不过我加了"^",所以可能优化为线性级n的了),还要许多次(取决于你想要的精度m),还不一定匹配得到,需要的时间O(m*n2)。下面介绍一种只要O(n)的算法:
以黑棋为例,首先从中间向两边检测,判断其连续为X或L的最大长度,只有其大于等于5时,黑棋才有可能在此条路线上五连,白棋同理,假如黑白棋都无法五连,那这个点(在这个方向的局势上)便没有任何下的意义,甚至连阻挡白棋都不需要,因为谁都无法五连。然后通过占比确定此路径上是白棋占优还是黑棋占优,以此决定是进入防御模式还是攻击模式:1)防御模式:假如此点下白棋,白棋组成的连数越高,得分越高;2)攻击模式:假如此点下黑棋,黑棋组成的连数越高,得分越高。此方法就只要数数量就可以看出局势的状况了
下面是代码,详细见注释:

protected int getScore(String str) {int lxLength = 0;// 从中间向两边为X或L的最大长度,该数字大于等于5时,X下此位置才有机会通过此线获胜int xNum = 0;// lxLength中X的个数,lxLength大于大于等于5且其中xNum占比越高,X下次位置进攻性越强int loLength = 0;// 从中间向两边为O或L的最大长度,该数字大于等于5时,O下此位置才有机会通过此线获胜int oNum = 0;// loLength中X的个数,loLength大于大于等于5且其中oNum占比越高,X越需要下次位置进行防守boolean canXAdd = true;// 表示lxLength是否还能增长,遇到O变falseboolean canOAdd = true;// 表示loLength是否还能增长,遇到X变falsefor (int i = 4; i < str.length(); i++) {if (str.charAt(i) == 'L') {if (canXAdd) {lxLength++;}if (canOAdd) {loLength++;}} else if (str.charAt(i) == 'X') {canOAdd = false;if (canXAdd) {lxLength++;xNum++;}} else if (str.charAt(i) == 'O') {canXAdd = false;if (canOAdd) {loLength++;oNum++;}}}canXAdd = true;canOAdd = true;for (int i = 3; i > 0; i--) {if (str.charAt(i) == 'L') {if (canXAdd) {lxLength++;}if (canOAdd) {loLength++;}} else if (str.charAt(i) == 'X') {canOAdd = false;if (canXAdd) {lxLength++;xNum++;}} else if (str.charAt(i) == 'O') {canXAdd = false;if (canOAdd) {loLength++;oNum++;}}}if (lxLength > 4 || loLength > 4) {StringBuilder stringBuilder = new StringBuilder(str);if (((float) xNum / (float) lxLength) >= ((float) oNum / (float) loLength)) {//判断谁占比高stringBuilder.setCharAt(4, 'X');//攻击模式:假定下黑棋,局势对自己越好,分数越高} else {stringBuilder.setCharAt(4, 'O');//防御模式:站在敌人的角度思考,假定下白棋,局势对白棋越优,就越需要由黑棋破环,分数越高}return getContinueCharNum(stringBuilder.toString()).getValue();//返回判断分数}return 1;}// 返回代表局势状态的枚举类型,weight中含有每个关键局势权重private Weight getContinueCharNum(String str) {boolean boundaryIsLRight = false;boolean boundaryIsLLeft = false;char c = str.charAt(4);int num = 0;for (int i = 4; i < str.length(); i++) {if (str.charAt(i) != c) {if (str.charAt(i) == 'L') {boundaryIsLRight = true;}break;}num++;}for (int i = 3; i >= 0; i--) {if (str.charAt(i) != c) {if (str.charAt(i) == 'L') {boundaryIsLRight = true;}break;}num++;}if (c == 'O') {if (num >= 5) {return Weight.OOOOO;} else if (num == 4) {if (boundaryIsLLeft && boundaryIsLRight) {return Weight.LOOOOL;}return Weight.OOOO;} else if (num == 3) {if (boundaryIsLLeft && boundaryIsLRight) {return Weight.LOOOL;}return Weight.OOO;} else if (num == 2) {return Weight.OO;} else {return Weight.O;}} else {if (num >= 5) {return Weight.XXXXX;} else if (num == 4) {if (boundaryIsLLeft && boundaryIsLRight) {return Weight.LXXXXL;}return Weight.XXXX;} else if (num == 3) {if (boundaryIsLLeft && boundaryIsLRight) {return Weight.LXXXL;}return Weight.XXX;} else if (num == 2) {return Weight.XX;} else {return Weight.X;}}  }

局势分数权重

分数的多少,对AI的判断至关重要,是AI胜率的直接因素。可以判断当黑棋可以五连时XXXXX分数应该是最高的,因为可以直接赢得比赛,假设为40000分。其次应该是OOOOO当白棋可以五连时,此处要是不下黑棋,导致白棋下了,就直接输了,因此也很重要,但是即便是四条路都被将军(虽然实际不可能),也不应该使其分数超过黑棋五连分,因为后者可以赢,而前者只能苟命,所以这里设为10000分。LXXXXL代表两边有空位的四连,可以说也是将军了,一旦下出来白棋无法挡住,这回合白棋不赢,下回合黑棋必赢,因此此分数适合设为2500分,同理LOOOOL设为625分合适,逻辑依次类推。
下面是经过测试,个人认为适合的分数,通过枚举实现:

public enum Weight {X(1), XX(11), XXX(45), LXXXL(180), XXXX(720), LXXXXL(2880), XXXXX(46080), O(1), OO(10), OOO(40), LOOOL(160),OOOO(600), LOOOOL(2400), OOOOO(11520);private final int value;private Weight(int value) {this.value = value;}public int getValue() {return value;}
}

设计模式

战局类Battle

属性有个15x15的char数组,只会填L(left 空位置),X(黑棋,AI使用的棋子),O(白棋,玩家使用的棋子)。前面提到过会出现E(error 表示越界)是因为在获取当前战局信息的时候会使用字符串,而第(1,2)位置(第二行第三列)的四条战局信息分别为:|:EEEL L LLLL,——:EELL L LLLL,\:EEEL L LLLL,/:EEEL L LLLL,这样可以保证即便是偏僻的位置,返回的字符串信息也是9位,方便处理。方法的话要一个下棋方法和一个返回战局的方法。

public class Battle {protected char battle[][];//战局protected Integration AI;//AIpublic Battle() {this.battle = new char[15][15];for (int i = 0; i < battle.length; i++) {for (int j = 0; j < battle.length; j++) {battle[i][j] = 'L';}}}public void setAI(Integration AI) {this.AI = AI;}public char getBattle(int a, int b) {if (a < 0 || a > 14 || b < 0 || b > 14) {return 'E';//越界返回E}return this.battle[a][b];}public void show() {System.out.print("\n\n\n");for (int i = 0; i < battle.length; i++) {for (int j = 0; j < battle.length; j++) {System.out.print(battle[i][j] + "  ");}System.out.print("\n");}}//下黑棋或白棋public void play(boolean isblack, int a, int b) {if (a < 0 || a > 14 || b < 0 || b > 14) {return;}if (isblack) {battle[a][b] = 'X';} else {battle[a][b] = 'O';if (AI != null) {AI.calculateScore(a, b);}}}}

AI类 Integration

首先他肯定要关联一个战局类Battle,时刻关注局势,然后自己再有一个分值表15x15的数组point,之前介绍过我们以空间换时间,因此这里还创建一个15x15x4的数组pointDetail 存放一个点横竖斜的分值,point就是四条线累加起来的最终分值。当然里面还需要一些方法:下棋,更新point表(每次白棋黑棋下完后立即更新),需求有这两个就够了,不过我写了好几个函数,命名我自己都分不清,代码太长了,这里就不贴了,下面给个链接下载,自己看吧。

UI设计

窗口类UIinterface (继承JFrame)

可以想到这个窗口是有多层的,最底下是棋盘,用张图片表示就行了,中间是棋子层,这里使用一个棋子Panel类,然后这个Panel里面塞15x15个按钮表示棋子就行了,最上层可以弹出获胜或是失败的标志。
关于分层,可以使用JLayeredPane类分层网格。通过在不同的层级插入容器就可以实现覆盖效果。

public class UIinterface extends JFrame {JLayeredPane layeredPane = new JLayeredPane(); // 分层网格JPanel chessboard;// 棋盘层(下层)PiecePanel piecePanel;// 棋子层(中层)BattlePane battlePane;ImageIcon image;public UIinterface() {image = new ImageIcon("./src/image/qipan.jpg");Integration AI = new Integration();piecePanel = new PiecePanel(image.getIconWidth(), image.getIconHeight());piecePanel.setOpaque(false);battlePane = new BattlePane();battlePane.setPanel(piecePanel);battlePane.setAI(AI);battlePane.setUI(this);AI.setBattle(battlePane);piecePanel.setBattlePane(battlePane);chessboard = new ChessBoard(image.getImage());JLabel label = new JLabel(image); // 把背景图片添加到标签里chessboard.setBounds(0, 0, image.getIconWidth(), image.getIconHeight()); // 把标签设置为和图片等高等宽chessboard.add(label);layeredPane.add(chessboard, JLayeredPane.DEFAULT_LAYER);// 棋盘层(下层)layeredPane.add(piecePanel, JLayeredPane.MODAL_LAYER);// 棋子层(中层)this.setTitle("五子棋");this.setBounds(0, 0, image.getIconWidth() + 15, image.getIconHeight() + 35);this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);this.setLayeredPane(layeredPane);this.setResizable(false);this.setVisible(true);}public void showWin() {JLabel label = new JLabel("YOU WIN!!!");label.setBounds(10, 180, 100, 100);label.setFont(new Font("Dialog", 1, 90));label.setSize(1000, 100);this.layeredPane.add(label, JLayeredPane.POPUP_LAYER);}public void showLose() {JLabel label = new JLabel("YOU LOSE!!!");label.setBounds(10, 180, 100, 100);label.setFont(new Font("Dialog", 1, 80));label.setSize(1000, 100);this.layeredPane.add(label, JLayeredPane.POPUP_LAYER);}class ChessBoard extends JPanel {// 绘制容器Image img;public ChessBoard(Image img) {this.img = img;}@Overrideprotected void paintComponent(Graphics g) {super.paintComponent(g);// 调用父类的高度和宽度g.drawImage(img, 0, 0, this.getWidth(), this.getHeight(), this);}}
}

棋子层类(继承JPanel)

里面塞15x15个棋子按钮就行,还要提供一个禁用所有按钮的方法,防止用户输赢后再下棋。

//棋子层类
public class PiecePanel extends JPanel {private static final long serialVersionUID = 1L;BattlePane battle;Piece pieces[][];public PiecePanel(int width, int height) {pieces = new Piece[15][15];for (int i = 0; i < pieces.length; i++) {for (int j = 0; j < pieces.length; j++) {pieces[i][j] = new Piece(i, j, this);this.add(pieces[i][j]);}}this.setBounds(0, 0, width, height);GridLayout gridLayout = new GridLayout(15, 15, 2, 2);this.setLayout(gridLayout);this.setOpaque(false);}public void setBattlePane(BattlePane battle) {this.battle = battle;}public void xiaqi(int a, int b) {this.battle.userPlay(a, b);}public void setDisable() {for (int i = 0; i < pieces.length; i++) {for (int j = 0; j < pieces.length; j++) {pieces[i][j].setEnabled(false);}}}
}

棋子按钮类(继承JButton)

这按钮有三种不同的状态,分别是空,白,黑。得注意一下如何往按钮里塞图片,让他变好看。this.setContentAreaFilled(false);可以清空填充,this.setOpaque(false);可以设置透明。点击时要设置图片(白棋或黑棋的图片),然后在禁用此按钮,防止用户往此位置下棋。不过在禁用的时候一定也要设置禁用图片,不然禁用按钮的图片会变色,也就是说setDisabledIcon和setIcon都要设置。

public class Piece extends JButton {int a, b;PiecePanel piecePanel;public Piece(int a, int b, PiecePanel piecePanel) {this.a = a;this.b = b;this.piecePanel = piecePanel;this.setContentAreaFilled(false);// 清空填充物this.setOpaque(false);// 设置透明this.addActionListener(new MyAction(this));}// 下棋触发的逻辑:更换按钮的图片,禁用按钮public void setPiece(boolean isBlack) {if (isBlack) {Icon blankIcon = new ImageIcon("./src/image/black.png");this.setDisabledIcon(blankIcon);this.setIcon(blankIcon);this.setEnabled(false);} else {Icon whiteIcon = new ImageIcon("./src/image/white.png");this.setDisabledIcon(whiteIcon);this.setIcon(whiteIcon);this.setEnabled(false);}}//按下按钮触发的事件:下棋class MyAction implements ActionListener {Piece p;public MyAction(Piece p) {this.p = p;}@Overridepublic void actionPerformed(ActionEvent e) {p.setPiece(false);p.removeActionListener(this);this.p.piecePanel.xiaqi(this.p.a, this.p.b);}}
}

总结

到这里所有的逻辑就介绍完了,还是有一些遗憾的:悔棋,重开之类的操作没写,而且代码有一点混乱,设计模式的使用也十分糟糕,命名也不太行,总感觉功能说不清楚又或是太相近,路漫漫其修远兮呀,不过这是算法作业来着,所以总体来说偏向算法的说明,如果您是冲着Swing来的话容我说声抱歉,可能交代的不是太清楚。
项目截图


下了许多把,总体是输多赢少,只有偶然才能“声东击西”赢得一把,说明算法权值还可以继续调整。对此AI对手的感受是:难缠,但有时候又会下一些让人意想不到的地方(直接跳到很远的地方下棋)。

项目下载

链接:人机五子棋
提取码:1je3

使用java swing制作人机五子棋相关推荐

  1. 如果用java swing编写一个五子棋(人人对战)

    2020博客之星年度总评选进行中:请为74号的狗子投上宝贵的一票! 我的投票地址:点击为我投票 写在前面: 大家好,我是 花狗Fdog ,来自内蒙古的一个小城市,目前在泰州读书. 很感谢能有这样一个平 ...

  2. 【使用java swing制作简易贪吃蛇游戏】软件实习项目二

    一.项目准备 需求分析: 实现贪吃蛇游戏基本功能,屏幕上随机出现一个"食物",称为豆子,上下左右控制"蛇"的移动,吃到"豆子"以后" ...

  3. Java Swing制作2048小游戏【完整版】

    技术讨论群:1005611884 可获取各种资料/代码,群内有Java/Python/C++/C#方向大佬,妹子多 /*** 技术 Swing* 实现思路:* 1. 绘制窗口* 2. 初始化游戏界面* ...

  4. java swing制作密钥生成器-AES对称加密

    本代码采用的是AES加密算法实现一个密钥生成器. 项目结构如下所示: [代码如下] LicenseEntry.java package cn.evansung.license;import java. ...

  5. java swing人机对战五子棋(含背景音乐)

    一.项目简介 本项目是一套基于java swing的人机对战五子棋系统,主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的Java学习者. 包含:项目源码.数据库脚本等,该项目附带全部源码可 ...

  6. 基于Java+Swing实现中国象棋游戏

    基于Java+Swing实现中国象棋游戏 一.系统介绍 二.功能展示 三.其他系统 四.获取源码 前言 中国象棋是起源于中国的一种棋,属于二人对抗性游戏的一种,在中国有着悠久的历史.由于用具简单,趣味 ...

  7. 基于Eclipse+Java+Swing+Mysql图书信息管理系统

    基于Eclipse+Java+Swing+Mysql图书信息管理系统 一.系统介绍 二.功能展示 1.主页 2.新增图书信息 3.删除图书信息 三.数据库 四.其他系统实现 五.获取源码 一.系统介绍 ...

  8. 基于Java+Swing+Mysql员工信息管理系统

    基于Java+Swing+Mysql员工信息管理系统 一.系统介绍 二.功能展示 1.主页 2.查询员工信息 3.删除员工信息 三.数据库 四.其他系统实现 五.获取源码 一.系统介绍 该系统实现了查 ...

  9. 基于Java+Swing+Mysql项目信息管理系统

    基于Java+Swing+Mysql项目信息管理系统 一.系统介绍 二.功能展示 1.主页 2.新增项目信息 3.删除项目信息 三.数据库 四.其他系统实现 五.获取源码 一.系统介绍 该系统实现了查 ...

最新文章

  1. 初学MyBatis-Plus
  2. Go 学习笔记(23)— 并发(02)[竞争,锁资源,原子函数sync/atomic、互斥锁sync.Mutex]
  3. 学以致用七---Centos7.2+python3.6.2+django2.1.1 --搭建一个网站(补充)
  4. ASP.NET 获取上一个页面的Url链接
  5. 简单描述ListView中使用GestureDetector冲突的解决办法
  6. 你真的了解Scrum吗?
  7. ASP.NET Core 简单实现七牛图片上传(FormData 和 Base64)
  8. centos 5.8 升级php5.1至5.3
  9. linux编译redis打包,linux下下载redis,并且编译
  10. 为什么需要 AtomicInteger 原子操作类?
  11. 【报告分享】2022年元宇宙全球年度(202页干货):蓄积的力量-北京大学.pdf(附下载链接)...
  12. 手机游戏开发 - 究竟要做什么、怎么做(中)
  13. bootstrap table 小计行_【2018/4/11】bootstrapTable添加一行
  14. 倒立摆的实现 1.前期准备
  15. 手机游戏连接计算机屏幕,教你如何把手机屏幕投到电脑上玩手游还能跟端游一样键鼠操作!...
  16. 无线网络通信技术完全介绍
  17. 看看中国科技巨头们在智能音箱行业的竞争
  18. Excel表格复制到Foxmail不显示边框
  19. 统计字母个数(java语言实现)
  20. 使用Mysql函数生成指定的自增序列号

热门文章

  1. 新垣结衣的孩子长啥样?用 StyleGAN 开源项目,一次看个够
  2. 练习 苹果-桔子线程【操作系统】
  3. 雷猴哇!用粤语还能写Python!
  4. html5视频常用API接口
  5. C语言输入未知数目的若干个整数
  6. 6代u笔记本完美支持win7_还真别说intel 九代CPU都能重装win7|九代CPU完美支持win7
  7. matlab sar adc建模,Simulink环境下的SAR ADC行为建模与仿真分析
  8. 主页设计中配色方案的使用
  9. 苹果xsmax是什么接口_质量绝对媲美苹果官方同款,液态硅胶手机壳,拿手里太舒服了~...
  10. 高薪程序员面试题精讲系列68之可重入锁、公平锁、自旋锁是怎么回事?