From d575ff3036fb8d509ce9e9596184327aa903bcc0 Mon Sep 17 00:00:00 2001 From: mengyxu Date: Tue, 15 Feb 2022 17:02:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 1.java/Java安全.md | 576 ++++++++++++++++ 1.java/数据结构与算法.md | 1133 +++++++++++++++++++++++++++++++ 2 files changed, 1709 insertions(+) create mode 100644 1.java/Java安全.md create mode 100644 1.java/数据结构与算法.md diff --git a/1.java/Java安全.md b/1.java/Java安全.md new file mode 100644 index 0000000..291a161 --- /dev/null +++ b/1.java/Java安全.md @@ -0,0 +1,576 @@ +#Java安全 +##为什么要掌握Java安全? +* 提升逼格,现在市场上的大部分程序员都对Java安全、数据加密等等一无所知,而现在的面试中很多面试官为了凸显自己的高逼格,总爱问你一些安全相关的问题。只有掌握了Java安全相关知识,才能跟面试官站在同一逼格线,进行愉快滴交流。 +* 项目中到处需要。如:账号密码的加密,订单支付的加密,个人资料的加密等等。 +* 初级工程师向中高级工程师进阶的必经之路。 +##什么是加密? +* 古老的加密方法:《潜伏》中的余则成,收到密码:12 25 13 15 29 19;《模仿游戏》,二战时期,德国的密码机及图灵为了破解德军密码而研制出第一代计算机(图灵机)。 +* 对称加密和非对称加密等等。 +* MD5,属于消息摘要(Message Digest)。 + +## 学习目标 +1. 复习字节与字符、字符编码、进制转换、java里的io +2. 一些基本的安全知识 +3. 三大风险:窃听风险、篡改风险、冒充风险 +4. 重放攻击(wpe) +5. 常用加密算法(对称/非对称) +6. 消息摘要 +7. 数字签名 +8. 数字证书 +9. Keytool工具的使用 +10. SSL/TLS的工作原理 +11. Https双向认证原理 + +#凯撒密码 +##第一个案例,对字符进行简单的加密。 +* 对字符数组中所有的字符做+1处理(后面可以转换成+num这个数由通讯双方来协定) +###代码 + + /** + * @author Camille + *对字符简单的加密 + */ + public class SimpleEncryptionDemo01 { + + public static void main(String[] args) { + // 确定加密的内容 + String content = "i love you tonight 404 see you z"; + //加密 + String encryptData = encrypt(content); + System.out.println("加密后:" + encryptData); + //解密 + String decryptData = decrypt(encryptData); + System.out.println("解密后:" + decryptData); + } + + /** + * 加密方法 + * + * @param content + * @return + */ + public static String encrypt(String content) { + // 将内容转换成字符数组 + char[] charArray = content.toCharArray(); + // 遍历出所有字符 + for (int i = 0; i < charArray.length; i++) { + // 对所有字符进行 + 1操作 + charArray[i] = (char) (charArray[i] + 1); + } + return new String(charArray); + } + + /** + * 解密方法 + * + * @param encryptData + * @return + */ + public static String decrypt(String encryptData) { + // 将内容转换成字符数组 + char[] charArray = encryptData.toCharArray(); + System.out.println(new String(charArray)); + // 解密 + for (int i = 0; i < charArray.length; i++) { + charArray[i] = (char) (charArray[i] - 1); + } + return new String(charArray); + } + } + +##第二个案例,对字节进行简单的加密。 + +**字节和字符的区别?** +* UTF-8编码下,一个英文或数字字符就是一个字节 +* UTF-8编码下,一个汉字代表三个字节。 +* 为什么new String(bytes)的时候回发生乱码,就是因为加密后得到的字节找不到对应的字符。 +* Base64的原理 +![](Base64yuanli.png) + +###代码 + + + /** + * @author Camille + *对字节进行加密 + */ + public class SimpleEncryptionDemo2 { + + public static void main(String[] args) { + // 测试字节字符 + String content = "中文"; + int key = 80; + String encryptData = encrypt(content,key); + System.out.println("+密后:" + encryptData); + String decryptData = decrypt(encryptData,key); + System.out.println("解密后:" + decryptData); + } + + /** + * 加密 + * + * @param content + * @return + */ + public static String encrypt(String content,int key) { + // 第一步,转换成字节数组 + byte[] bytes = content.getBytes(); + System.out.println("打印未加密的字节数组:"); + Util.printBytes(bytes); + System.out.println("_________________________________________"); + // 第二步,遍历出所有字节,并做+1处理 + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) (bytes[i] + key); + } + System.out.println("打印加密的字节数组:"); + Util.printBytes(bytes); + System.out.println("_________________________________________"); + // return new String(bytes); + return Base64.getEncoder().encodeToString(bytes); + } + + /** + * 解密 + * @param encryptData + * @return + */ + public static String decrypt(String encryptData,int key) { + // 第一步,转换成字节数组,加密时使用Base64编码,那解密时也要使用Base64解码 + byte[] bytes = Base64.getDecoder().decode(encryptData); + // byte[] bytes = encryptData.getBytes(); + System.out.println("打印经过两次转换后的字节数组:"); + Util.printBytes(bytes); + System.out.println("_________________________________________"); + // 解密 + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) (bytes[i] - key); + } + System.out.println("打印解密后的字节数组:"); + Util.printBytes(bytes); + System.out.println("_________________________________________"); + return new String(bytes); + } + } + +##对称加密与非对称加密 +* 对称加密与非对称加密的区别 + * 对称加密。同一把钥匙进行加密和解密 + * 非对称加密,公钥加密,私钥解密。 + +##对称加密的案例 +常见算法:AES、DES、3DES、TDEA、Blowfish、RC2、RC4、RC5、IDEA、SKIPJACK +AES:高级加密标准(Advanced Encryption Standard) +DES:数据加密标准(Data Encryption Standard) +###对称加密第一个案例,代码,。 + public class SymmetricalEncryptionDemo01 { + + public static void main(String[] args) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, + IllegalBlockSizeException, BadPaddingException { + // 声明要加密的内容 + String content = "今晚404,不见不散"; + //生成key + SecretKey key = createKey(); + //加密 + String encryptData = encrypt(content, key); + System.out.println(encryptData); + //解密 + String decryptData = decrypt(encryptData, key); + System.out.println(decryptData); + } + + /** + * 生成key + * @return + * @throws NoSuchAlgorithmException + */ + public static SecretKey createKey() throws NoSuchAlgorithmException { + // 生成key + KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); + SecretKey secretKey = keyGenerator.generateKey(); + return secretKey; + } + + /** + * 加密方法 + * + * @param content + * @return + * @throws InvalidKeyException + * @throws NoSuchPaddingException + * @throws NoSuchAlgorithmException + * @throws BadPaddingException + * @throws IllegalBlockSizeException + */ + public static String encrypt(String content, SecretKey key) throws InvalidKeyException, NoSuchAlgorithmException, + NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException { + // 获取cipher单例对象 + Cipher cipher = Cipher.getInstance("AES"); + // 初始化cipher,设置为加密模式 + cipher.init(Cipher.ENCRYPT_MODE, key); + // 开始加密 + byte[] encryptBytes = cipher.doFinal(content.getBytes()); + return Base64.getEncoder().encodeToString(encryptBytes); + } + + /** + * @param encryptData + * @return + */ + public static String decrypt(String encryptData, SecretKey key) throws InvalidKeyException, + NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException { + byte[] encryptBytes = Base64.getDecoder().decode(encryptData); + // 获取cipher单例对象 + Cipher cipher = Cipher.getInstance("AES"); + // 解密 + cipher.init(Cipher.DECRYPT_MODE, key); + byte[] decryptBytes = cipher.doFinal(encryptBytes); + return new String(decryptBytes); + } + } + +###对称加密的第二个案例,将生成的key保存到本地,然后解密时和下一次加密时就只需要到本地读取即可。。 + + /** + * @author Camille + *保存key到本地 + */ + public class SymmetricalEncryptionDemo02 { + + public static void main(String[] args) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, + IllegalBlockSizeException, BadPaddingException, IOException, ClassNotFoundException { + // 声明要加密的内容 + String content = "今晚404,不见不散"; + //第一次生成key,并序列化到本地 + // SecretKey key = createKey(); + // SerializableUtil.saveObject2File("heima.key", key); + //从文件中读取key + SecretKey key = (SecretKey) SerializableUtil.readObjectFromFile("heima.key"); + System.out.println(key); + //加密 + String encryptData = encrypt(content, key); + System.out.println(encryptData); + //解密 + String decryptData = decrypt(encryptData, key); + System.out.println(decryptData); + } + + /** + * 生成key + * @return + * @throws NoSuchAlgorithmException + */ + public static SecretKey createKey() throws NoSuchAlgorithmException { + // 生成key + KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); + SecretKey secretKey = keyGenerator.generateKey(); + return secretKey; + } + + /** + * 加密方法 + * + * @param content + * @return + * @throws InvalidKeyException + * @throws NoSuchPaddingException + * @throws NoSuchAlgorithmException + * @throws BadPaddingException + * @throws IllegalBlockSizeException + */ + public static String encrypt(String content, SecretKey key) throws InvalidKeyException, NoSuchAlgorithmException, + NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException { + // 获取cipher单例对象 + Cipher cipher = Cipher.getInstance("AES"); + // 初始化cipher,设置为加密模式 + cipher.init(Cipher.ENCRYPT_MODE, key); + // 开始加密 + byte[] encryptBytes = cipher.doFinal(content.getBytes()); + return Base64.getEncoder().encodeToString(encryptBytes); + } + + /** + * @param encryptData + * @return + */ + public static String decrypt(String encryptData, SecretKey key) throws InvalidKeyException, + NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException { + byte[] encryptBytes = Base64.getDecoder().decode(encryptData); + // 获取cipher单例对象 + Cipher cipher = Cipher.getInstance("AES"); + // 解密 + cipher.init(Cipher.DECRYPT_MODE, key); + byte[] decryptBytes = cipher.doFinal(encryptBytes); + return new String(decryptBytes); + } + } +**保存对象到文件** + + /** + * 保存对象到本地 + * + * @param obj + * @throws IOException + */ + public static void saveObject2File(String fileName, Object obj) throws IOException { + FileOutputStream fos = new FileOutputStream(new File(fileName)); + ObjectOutputStream oos = new ObjectOutputStream(fos); + oos.writeObject(obj); + } +**从文件中读取对象** + + /** + * 从文件中读取对象 + * @param fileName + * @return + * @throws IOException + * @throws ClassNotFoundException + */ + public static Object readObjectFromFile(String fileName) throws IOException, ClassNotFoundException { + FileInputStream fis = new FileInputStream(new File(fileName)); + ObjectInputStream ois = new ObjectInputStream(fis); + Object object = ois.readObject(); + return object; + } +###第三个案例——自定义秘钥,代码 + + /** + * 生成key + * @return + * @throws NoSuchAlgorithmException + */ + public static SecretKey createKey(String keyword) throws NoSuchAlgorithmException { + // 生成key + KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); + keyGenerator.init(new SecureRandom(keyword.getBytes())); + SecretKey secretKey = keyGenerator.generateKey(); + return secretKey; + } + +###应用场景 +* 登录,post请求{username=lisi,pw=加密} + +##非对称加密的案例 +常见算法:RSA、Elgamal、背包算法、Rabin、D-H、ECC(椭圆曲线加密算法)等 +应用场景:银行和电商网站,他就采用非对称加密,将公钥给所有人,你们就用这个公钥加密,私钥我自己留着,谁也不知道,所以除了我,谁也解密不了。 +###第一个案例(数据量不大的情况下可用,加密数据小于117个字节,解密数据小于128个字节) + public class AysmmetricEncryptionDemo { + + public static void main(String[] args) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, + IllegalBlockSizeException, BadPaddingException { + String content = "今晚小树林,等你哦"; + //生成密钥对 + KeyPair keyPair = createKeyypair(); + //获取公钥私钥 + PublicKey publicKey = keyPair.getPublic(); + PrivateKey privateKey = keyPair.getPrivate(); + //加密 + String encryptData = encrypt(content, publicKey); + System.out.println("加密后:" + encryptData); + //解密 + String decryptData = decrypt(encryptData, privateKey); + System.out.println("解密后:" + decryptData); + } + /** + * 生成秘钥对 + * @return + * @throws NoSuchAlgorithmException + */ + public static KeyPair createKeyypair() throws NoSuchAlgorithmException{ + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + return keyPair; + } + /** + * 加密 + * + * @param content + * @return + * @throws NoSuchPaddingException + * @throws NoSuchAlgorithmException + * @throws BadPaddingException + * @throws IllegalBlockSizeException + * @throws InvalidKeyException + */ + public static String encrypt(String content, PublicKey publicKey) throws NoSuchAlgorithmException, + NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException { + // 获取cipher对象 + Cipher cipher = Cipher.getInstance("RSA"); + // 初始化cipher,指定模式为加密,传入公钥 + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + byte[] encryptBytes = cipher.doFinal(content.getBytes()); + return Base64.getEncoder().encodeToString(encryptBytes); + } + + /** + * 解密方法 + * + * @param encryptData + * @param privateKey + * @return + * @throws NoSuchPaddingException + * @throws NoSuchAlgorithmException + * @throws InvalidKeyException + * @throws BadPaddingException + * @throws IllegalBlockSizeException + * @throws Exception + */ + public static String decrypt(String encryptData, PrivateKey privateKey) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException{ + byte[] encryptBytes = Base64.getDecoder().decode(encryptData); + // 获取cipher对象 + Cipher cipher = Cipher.getInstance("RSA"); + // 小芝解密 + cipher.init(Cipher.DECRYPT_MODE, privateKey); + byte[] decryptBytes = cipher.doFinal(encryptBytes); + return new String(decryptBytes); + } + } +###如果数据量大的话,则要对要加密、解密的数据进行分块处理,代码如下: + + /** + * 处理大量数据时,分块加密/解密 + * @param content + * @param cipher + * @param max + * @return + * @throws IllegalBlockSizeException + * @throws BadPaddingException + * @throws IOException + */ + private static byte[] doFinalWithBlock(byte[] bytes, Cipher cipher,int max) + throws IllegalBlockSizeException, BadPaddingException, IOException { + int len = bytes.length;//3000 + //加密数据长度不能超过117个byte + int inputOffset = 0;//2700 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + while (len > inputOffset) { + //特殊情况,最后一次剩下的数据不足117 + if ((len - inputOffset) >= max) { + byte[] encryptBytes = cipher.doFinal(bytes, inputOffset, max); + baos.write(encryptBytes); + inputOffset += max; + }else { + byte[] encryptBytes = cipher.doFinal(bytes, inputOffset, len - inputOffset); + baos.write(encryptBytes); + inputOffset = len; + } + } + byte[] byteArray = baos.toByteArray(); + return byteArray; + } + +##消息摘要 +消息摘要是一个不可逆的过程。常用来防篡改。 + +常见算法:MD5、SHA、CRC等 + +###byte数组和16进制字符串的互转 + + /** + * byte数组转16进制字符串 + * @param bytes + * @return + */ + public static String bytes2Hex(byte[] bytes){ + StringBuffer sBuffer = new StringBuffer(); + for (int i = 0; i < bytes.length; i++) { + //取高位 + int high = (bytes[i] & 0xf0) >> 4; + //取低位 + int low = bytes[i] & 0x0f; + sBuffer.append(HEXSTR[high]).append(HEXSTR[low]); + } + return sBuffer.toString(); + } + + /** + * 16进制字符串转字节数组 + * @param hex + * @return + */ + public static byte[] hex2Bytes(String hex){ + int len = hex.length()/2; + //声明一个字节数组用于接收转换后的字节 + byte[] bytes = new byte[len]; + for (int i = 0; i < len; i++) { + //首先取高位,取偶数位 + String highStr = hex.substring(2 * i, 2 * i + 1); + String lowStr = hex.substring(2 * i + 1, 2 * i + 2); + int high = Integer.parseInt(highStr, 16) << 4; + int low = Integer.parseInt(lowStr, 16); + bytes[i] = (byte) (high + low); + } + return bytes; + } + +##数字签名 + +**签名过程** + + String content = "404房卡"; + Signature signature = Signature.getInstance("MD5withRSA"); + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + PrivateKey privateKey = keyPair.getPrivate(); + PublicKey publicKey = keyPair.getPublic(); + signature.initSign(privateKey); + signature.update(content.getBytes()); + byte[] sign = signature.sign(); + +###数字签名的流程(画图理解) +###Signature** + + //小芝认证 + signature.initVerify(publicKey); + //我们要对房卡进行认证 + // String content2 = "505的房卡"; + signature.update(content.getBytes()); + boolean verify = signature.verify(sign); + Util.printBytes(sign); + System.out.println(verify); +##keytool的使用。 +* 生成keyPair keytool -genkeypair +* 修改别名 keytool -changealias -alias mykey -destalias heima1 +* 导出证书 keytool -exportcert +* 导入证书 keytool -importcert + +**获取文件中的证书** + + // 获取证书工厂的实例 + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + FileInputStream inStream = new FileInputStream(new File("heima.cer")); + X509Certificate certificate = (X509Certificate) cf.generateCertificate(inStream); + +##SSl +**双向认证的原理** +**代码访问12306** + + public class HttpsDemo { + + public static void main(String[] args) throws IOException, NoSuchAlgorithmException, KeyManagementException, KeyStoreException, CertificateException { + //第一种方式,我所有的证书都不检查 + SSLContext sslContext = SSLContext.getInstance("TLS");//TLS 是transport Layer safe + //获取信任管理者工厂 + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + //获取keyStore + KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); + ks.load(null); + //从文件中读取证书 + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + FileInputStream inStream = new FileInputStream(new File("srca.cer")); + X509Certificate cert = (X509Certificate) cf.generateCertificate(inStream); + //给keyStore设置证书 + ks.setCertificateEntry("12306", cert); + trustManagerFactory.init(ks); + TrustManager[] tm = trustManagerFactory.getTrustManagers(); + sslContext.init(null, tm, null); + SSLSocketFactory socketFactory = sslContext.getSocketFactory(); + HttpsURLConnection.setDefaultSSLSocketFactory(socketFactory); + // TODO Auto-generated method stub + URL url = new URL("https://kyfw.12306.cn/otn/"); + //第二步,获取coon + HttpsURLConnection coon = (HttpsURLConnection) url.openConnection(); + InputStream inputStream = coon.getInputStream(); + String response = Util.inputStream2String(inputStream); + System.out.println(response); + } + } \ No newline at end of file diff --git a/1.java/数据结构与算法.md b/1.java/数据结构与算法.md new file mode 100644 index 0000000..4823ad5 --- /dev/null +++ b/1.java/数据结构与算法.md @@ -0,0 +1,1133 @@ +# 课程目标 # +## 目的 ## +* 找工作,面试中经常会出现数据结构和算法的问题,尤其是笔试中经常出现。 +* 数据结构与算法这门课程是初级程序员向中高级程序员迈进的必经之路。 +* 要进大公司,数据结构与算法是不可或缺的。 +* 有时候看别人的源码的时候,经常会涉及到。 +## 基本算法 ## + * 冒泡排序 + * 选择排序 + * 插入排序 +## 递归算法 ## + * 自然数的累加问题(1+....+99+100) + * 兔子繁衍问题:已知一对兔子每一个月可以生一对小兔子,而一对兔子出生后.第三个月开始生小兔子假如一年内没有发生死亡,则一对兔子一年内能繁殖成多少对? + * 汉诺塔 + * 快速排序算法 +## 数据结构与算法的关系 ## + * 循环数数,n个人围成一圈报数,从1开始,凡报到3的退出,问留到最后的是几号? + * 括号算法(简单的编译器),一行字符串有小括号、中括号、大括号,计算括号匹配与否,是否错位。 + * 计算器的算法。四则运算 +* 双链表 +* 二叉树 + +# 三种基本的算法 # +## 冒泡排序 ## + +**从第一个数据开始,两两比较,如果左边的大于右边的,那就交换两个数据的位置,依次这样往下比较。会找到一个最大值放到数据的最后,然后下一次按上述方式继续冒泡。冒泡次数为n-1。** + +![bubble](bubbleSort.png) + + public static void bubbleSort(int[] datas){ + //外层循环,控制排序次数 + for (int j = datas.length - 1; j > 0 ; j--) { + //内层循环,控制冒泡次数 + for (int i = 0; i < j; i++) { + if (datas[i] > datas[i + 1]) { + //交换两个数的位置 + change(datas,i, i + 1); + } + } + } + } + +## 选择排序 ## +**一次过程:(从小到大),从所有数据中找最小数据的下标,找到下标和第一个位置交换。** +![select](selectSort.png) + + /** + * 选择排序 + * @param datas + */ + public static void selectSort(int[] datas){ + //外层循环,从0开始 + for (int i = 0; i < datas.length - 1; i++) { + int minIndex = i; + for (int j = i + 1; j < datas.length; j++) { + if (datas[j] < datas[minIndex]) { + minIndex = j; + } + } + change(datas, minIndex, i); + } + } +## 插入排序 ## +**从第二个位置开始,依次向前比较所有数据,一直找到比该数据小的位置,结束过程,并把该数据插入到(比该数据小)的位置后面** + + /** + * 插入排序 + * @param datas + */ + public static void insertSort(int[] datas){ + //外层循环控制执行次数 + for (int i = 1; i < datas.length; i++) { + int temp = datas[i]; + int j = i - 1; + for (; j >= 0; j--) { + if (datas[j] > temp) { + datas[j + 1] = datas[j]; + }else { + break; + } + } + datas[j + 1] = temp; + } + } + +# 递归算法 # + +**递归效率高的原因:直接在内存中进行压栈和出栈处理** + +## 一个简单的递归例子 ## + + /** + * 求1到的和num + * @param num + * @return + */ + public static int sum(int num){ + if (num < 1) { + throw new RuntimeException("请按套路出牌!"); + }else if (num == 1) { + return 1; + }else { + return num + sum(num - 1); + } + } +## 兔子繁衍问题:已知一对兔子每一个月可以生一对小兔子,而一对兔子出生后.第三个月开始生小兔子假如一年内没有发生死亡,则一对兔子一年内能繁殖成多少对? ## + /** + * 求第n个月的兔子对数 + * @param n + * @return + */ + public static int tuzi(int n){ + if (n == 1 || n == 2) { + return 1; + }else { + return tuzi(n-1) + tuzi(n-2); + } + } +![tuzi](tuzi.png) +## 汉诺塔问题 ## + + /** + * 汉诺塔,n个盘,从a柱借助b柱移动到c柱 + * @param n + * @param a + * @param b + * @param c + */ + public static void TowerOfHanoi(int n,char a,char b, char c){ + if (n == 1) { + System.out.println("盘" + n + "从" + a + "柱移动到" + c + "柱"); + }else { + TowerOfHanoi(n - 1, a, c, b); + System.out.println("盘" + n + "从" + a + "柱移动到" + c + "柱"); + TowerOfHanoi(n-1, b, a, c); + } + } + +## 快速排序 ## + +**以最后一个数据做参照物,定义左右指针(可以理解成下标),左指针从左往右依次找数据,找比参照物大的数据,右指针从右往左找比参照物小的数据,找到就停止,如果左右指针没有交叉,就交换左右指针的位置上的值,继续找数据,如果交叉,终止,交换左指针和参照物的值。由左指针把数据分成两半,每一半按同样的方式去找。** + + /** + * @param datas + * @param left + * 数据的起始位置 + * @param right + * 数据的结束位置 + */ + public static void quickSort(int[] datas,int left,int right){ + //跳出递归的条件,如果交叉 + if (left > right) { + return; + } + //否则,及进行快速排序 + int middle = findMiddle(datas, left, right); + //每一半按照同样的方式排序 + //左一半的数据 + quickSort(datas, left, middle - 1); + //右一半 + quickSort(datas, middle + 1, right); + } + /** + * 找中间位置 + * @param datas + * @param left + * @param right + * @return + */ + public static int findMiddle(int[] datas,int left,int right){ + int middle = -1; + //以最后一个数据作为参照物 + int temp = datas[right]; + int leftIndex = left; + int rightIndex = right - 1; + while (true) { + //1.左指针从左往右找比temp大的 + while (leftIndex < right && datas[leftIndex] <= temp) { + //跳出循环的条件有两个,第一个是leftIndex == right,第二个datas[leftIndex] >temp + //如果通过 leftIndex == right跳出循环,说明参照物是最大值,那么middle就是right。 + if (leftIndex == right) { + middle = leftIndex; + break; + } + leftIndex ++; + } + while(rightIndex >= left && datas[rightIndex] >= temp){ + //如果通过rightindex >= left跳出循环,说明参照物是最小的 + //这个条件也可以按交叉处理,按交叉的处理 + rightIndex --; + } + if (leftIndex < rightIndex) { + //没有交叉 + //交换左右指针上的数据 + //继续循环 + change(datas, leftIndex, rightIndex); + continue; + }else { + //交叉 + //交换左指针与参照物的值 + change(datas, leftIndex, right); + //找到中间点了(leftIndex),终止整个循环 + middle = leftIndex; + break; + } + } + return middle; + } + +**四种排序的速度测试** + +# 数据结构和算法的关系 # +## 连续数数的案例(队列) ## +### 使用数组实现 ### +![arrayCount](suzuCount.png) + + /** + * 用数组解决数数的问题 + * + * @param personNum + * 人数 + * @param num + * 数到num退出 + */ + public static void count(int personNum, int num) { + // 将人放到数组中 + int[] persons = new int[personNum]; + for (int i = 0; i < persons.length; i++) { + persons[i] = i + 1; + } + // 定义一些变量 + int duns = 0;//蹲下的人数 + int dunCounts = 0;//蹲下了,但是还数数了的次数,就是多余的数数次数 + int index = 0;//数到了几 + while(duns != personNum){ + //如果不是全部人都蹲下了,那就继续数数 + //判断当前数数是否是蹲下人数数 + if (persons[index % personNum] == 0) { + //如果是,则 + dunCounts ++; + //继续数数 + index ++; + }else { + ///判断当前数数位置是否需要蹲下 + if ((index + 1 - dunCounts) % num == 0) { + //如果需要 + System.out.println(persons[index % personNum] + "滚出去!!!"); + persons[index % personNum] = 0; + duns ++; + //继续数数 + index ++; + }else { + //否则,继续数数 + index ++; + } + } + } + } + +### 队列数数 ### + + /** + * 用队列的方式来数数 + * @param personNum + * @param num+ + */ + public static void count2(int personNum,int num){ + //将人放入队列中 + Queue queue = new LinkedList(); + for(int i=0;i getNums(String exp) { + List nums = new ArrayList(); + StringTokenizer sTokenizer = new StringTokenizer(exp, "+-*/"); + while (sTokenizer.hasMoreElements()) { + String str = sTokenizer.nextElement().toString().trim(); + //如果str的第一个字符是@,那么转成负号 + if (str.charAt(0) == '@') { + str = "-" + str.substring(1); + } + double d = Double.parseDouble(str); + nums.add(d); + } + return nums; + } +### 提取运算符集合 ### + /** + * 提取运算符 + * @param exp + * @return + */ + private static List getOpts(String exp) { + List opts = new ArrayList(); + StringTokenizer sTokenizer = new StringTokenizer(exp, "0123456789.@"); + while (sTokenizer.hasMoreElements()) { + String str = sTokenizer.nextElement().toString().trim(); + opts.add(str.charAt(0)); + } + return opts; + } +### 不带括号的四则运算 ### +* 1.0先将“-”转换成“@”。 +* 2.0提取数字集合。 +* 3.0提取字符集合。 +* 4.0先乘除。 +* 5.0后加减。 + + /** + * 不带括号的四则运算 + * @param exp + * @return + */ + public static double calc(String exp){ + //负号转成@ + exp = fu2At(exp); + //提取数字集合 + List nums = getNums(exp); + //提取运算符集合 + List opts = getOpts(exp); + //先乘除 + //遍历出所有的*/号 + for(int i=0;i"-3+(3*-6)-8/(-4-3)" + exp = exp.substring(0,leftIndex) + d + exp.substring(rightIndex + 1); + System.out.println(exp); + //对exp做递归运算 + return calcKuohao(exp); + }else { + //说明没有括号 + return calc(exp); + } + } + + +# 自定义数据结构 # + +## 单链表 ## +**链表添加数据的步骤** + +* 新建节点。 +* 把数据放到节点中。 +* 把节点放入链表中。 + +**应用场景** + +* 删除时,只适合删除第一个元素; + +* 添加时,只直接添加到最后一个元素的后面或者添加到第一个元素的前面; + +* 属于单向迭代器,只能从一个方向走到头(只支持前进或后退,取决于实现),查找效率极差。不适合大量查询的场合。 + +***这种典型的应用场合是各类缓冲池和栈的实现。*** +## 双链表 ## + +### 双链表和单链表的区别和应用场景 ### +***单链表只能查找下一个数据,双链表可以查找上一个和下一个节点的数据*** + +***应用场景*** + +* 删除时,可以删除任意元素,而只需要极小的开销; + +* 添加时,当知道它的前一个或后一个位置的元素时,只需要极小的开销,可以从任意位置添加。 + +* 属于双向迭代器,可以从头走到尾或从尾走到头,但同样查找时需要遍历,效率与单向链表无改善,不适合大量查询的场合。 + +***这种典型的应用场景是各种不需要排序的数据列表管理。*** +## 添加数据 ## +### 双链表从尾部添加数据 ### + /** + * 从尾部添加数据 + * @param data + */ + public void addFromRear(Object data){ + //将数据放入节点中 + Node node = new Node(data); + //将节点放入链表中 + if (head == null) { + //如果头节点为空,说明链表为空,那么头和尾节点都是node + head = node; + rear = node; + }else { + //若有头节点 + //尾节点的下一个节点是node + rear.next = node; + //node的上一个节点是rear + node.prev = rear; + //将node设为尾节点 + rear = node; + } + } + +### 打印双链表 ### + @Override + public String toString() { + StringBuilder sBuilder = new StringBuilder("["); + //拼接数据 + //遍历链表 + Node node = head; + //如果头节点不为空则,一直遍历 + while(node != null){ + if (node != rear) { + sBuilder.append(node.data + ","); + }else{ + sBuilder.append(node.data + "]"); + } + //变更node,要不就死循环了 + node = node.next; + } + return sBuilder.toString(); + } +### 从头部添加数据 ### + /** + * 从头部添加数据 + * @param data + */ + public void addFromHead(Object data){ + //创建新节点,把数据放入节点中 + Node node = new Node(data); + //将节点放入链表中 + if (head == null) { + //如果头节点为空,说明链表没数据 + head = node; + rear = node; + }else { + //如果链表有数据 + //从头部添加数据,头节点的前一个节点就是node + head.prev = node; + //node的下一个节点是head + node.next = head; + //将node变成head + head = node; + } + } +### 排序添加数据(添加进来后自动排序) ### +* 1.object比较数据大小 +* 2.找位置,(从小到大),找比它大的第一个节点。 +* 3.如果没找到比它大的第一个节点,那就说明data最大,则放到链表尾部。 +* 4.如果找到了 + + /** + * 添加数据并排序 + * @param data + */ + public void addSort(Object data){ + //创建节点,将数据放入节点中 + Node node = new Node(data); + //找位置 + Node next = findNext(data); + if (next == null) { + //如果找不到比它大的,说明他是最大的\ + //那么添加到尾部 + addFromRear(data); + }else { + if (next == head) { + addFromHead(data); + }else { + //否则,将node插入中间,指定node的prev和next,并有一个节点的prev和一个节点的next指向node + //next的上一个节点的下一个节点是node + next.prev.next = node; + //node的下一个节点是next + node.next = next; + //node的上一个节点是next的上一个节点 + node.prev = next.prev; + //将它添加到next的前面 + next.prev = node; + } + } + } +### 比较两个Object的大小 ### + /** + * 比较两个数据的大小 + * @param d1 + * @param d2 + * @return + */ + public int compare(Object d1,Object d2){ + Comparable c1 = null; + Comparable c2 = null; + //如果Object类型实现了比较器 + if (d1 instanceof Comparable && d2 instanceof Comparable) { + c1 = (Comparable) d1; + c2 = (Comparable) d2; + }else { + c1 = d1.toString(); + c2 = d2.toString(); + } + return c1.compareTo(c2); + } +### 查找比data大的第一个节点 ### + /** + * 查找比data大的第一个节点 + * @param data + * @return + */ + public Node findNext(Object data){ + //从头节点开始遍历 + Node node = head; + while(node != null){ + if (compare(node.data, data) > 0) { + //如果next的数据比data大 + break; + }else { + //否则,继续找 + node = node.next; + } + } + return node; + } + +## 删除数据 ## + +### 1.查找数据所在的节点 ### + /** + * 查找data所在的节点 + * @param data + * @return + */ + private Node findNode(Object data) { + //从头节点开始遍历 + Node node = head; + while(node != null){ + if (node.data.equals(data) && node.data.hashCode() == data.hashCode()) { + //找到了就跳出循环 + break; + }else { + //否则,继续找 + node = node.next; + } + } + //返回node,如果找到了,break跳出那就返回找到的node,如果找不到node == null跳出,那就返回null。 + return node; + } +### 2.删除该数据所在的节点 ### +* 只有一个节点 +* 删除头节点 +* 删除尾节点 +* 删除中间节点 + + /** + * 删除某个节点 + * @param node + */ + public void delete(Node node){ + //1.如果只有一个节点 + if (node == head && node == rear) { + //要删除的话,让头节点和尾节点为空即可 + head = null; + rear = null; + }else if (node == head) { + //2.如果该节点是头节点,说明后面肯定有节点 + //断开头节点对node的引用 + head = head.next; + //断开下一个节点的prev对node的引用 + head.prev = null; + }else if (node == rear) { + //3.如果该节点是尾节点,说明前面肯定还有节点 + //断开尾节点对node的引用 + rear = rear.prev; + //断开前一个节点的next对node的引用 + rear.next = null; + }else { + //4.如果该节点是中间节点,说明两边肯定都有节点 + //node的前一个节点的next指向node的下一个节点 + node.prev.next = node.next; + //node的后一个节点的prev指向node节点的前一个节点 + node.next.prev = node.prev; + } + } + +## 修改数据 ## +### 1.传入oldData和newData来删除数据 ### + /** + * 更新数据 + * @param oldData + * @param newData + */ + public void updata(Object oldData,Object newData){ + //先找到oldData所在的节点 + Node node = findNode(oldData); + if (node != null) { + node.data = newData; + } + } +### 2.只传入newData来删除数据 ### +**应用场景:特定的一些场景,比如要修改一个人的名字,我们根据传入的人的身份证号来获取这个人的信息,然后再修改名字** + + public class Person { + public String id; + public char sex; + public String name; + public Person(String id,char sex,String name) { + this.id = id; + this.name = name; + this.sex = sex; + } + @Override + public boolean equals(Object obj) { + if (obj instanceof Person) { + //如果是Person类型 + //比较身份证号,身份证一样才是一样的 + Person p = (Person) obj; + return this.id.equals(p.id); + }else { + //如果不是人,那直接不一样 + return false; + } + } + @Override + public int hashCode() { + return id.hashCode(); + } + @Override + public String toString() { + return this.name; + } + } + +## 查找数据 ## +### 是否包含某数据:contains()方法 ### +### 迭代器 ### +**实现Iterable接口** + + @Override + public Iterator iterator() { + class MyIterator implements Iterator{ + //从头节点开始遍历 + Node node = head; + @Override + public boolean hasNext() { + //是否有节点 + return node != null; + } + + @Override + public Object next() { + //先获取node的数据 + Object data = node.data; + //将node设为下一个节点 + node = node.next; + return data; + } + @Override + public void remove() { + //调用next方法之后,node就变成了以前的node的下一个节点(此处要考虑此时的node是否为空) + //那就是要删除现在的node的上一个节点 + if (node != null) { + delete(node.prev); + }else { + //如果node为空,说明要删除的节点是尾节点 + delete(rear); + } + } + } + return new MyIterator(); + } +## 双链表封装成泛型 ## + +## 链表实现队列 ## +**队列,从队列尾部添加数据,从队列头移除数据** + +* size()方法 + + @Override + public int size() { + return datas.count; + } +* isEmpty()方法 + + //MyQueue的isEmpty方法 + @Override + public boolean isEmpty() { + return datas.isEmpty(); + } + + + + //MyDoubleLink的isEmpty方法 + public boolean isEmpty(){ + return head == null; + } +* add()方法 + + @Override + public boolean add(E e) { + datas.addFromRear(e); + return true; + } + +* poll()方法 + + @Override + public E poll() { + //取出数据 + E data = datas.deleteFromHead(); + return data; + } + + + + /** + * 从头节点移除数据,并返回删除的数据 + * @param data + * @return + */ + public E deleteFromHead(){ + E data = null; + if (head != null){ + data = head.data; + //下一个数据成为头节点 + head = head.next; + //判断后面没有 + if (head != null) + head.prev = null; + else { + //没有数据 + rear = null; + } + } + return data; + } +## 链表实现堆栈 ## +**栈,从尾部添加数据,从尾部移除数据** + + public class MyStack { + private MyDoubleLink2 datas = new MyDoubleLink2(); + /** + * 压栈 + * @param data + */ + public void push(E data){ + //从尾部添加数据 + datas.addFromRear(data); + } + /** + * 出栈 + * @return + */ + public E pop(){ + //从尾部取数据 + E data = datas.deleteFromRear(); + return data; + } + public boolean isEmpty(){ + return datas.isEmpty(); + } + } +## 二叉树 ## + +### 基本特点 ### +每个节点最多有两个子节点,除了根节点之外,每个节点都有一个唯一的父节点。 + +### 有序二叉树 ### +查找快,删除数据也快。 + +**数据库中的索引,例如b-树索引** + +**二叉树遍历** + +* 前序: 根节点在前 +* 中序: 根节点在中 +* 后序: 根节点在后 + +**二叉树有序、不可重复的添加节点** + +* 判断data是否存在(查找data所在的节点),如果存在,则return +* 否则,创建节点。 +* 将数据放入节点中。 +* 找父节点。:判断如果root == null,那么添加节点为root。 +* 否则,就找父节点。 +* 将找到的父节点设为新节点的父节点。 +* 比较新增数据和父节点数据的大小,如果新增数据大,那么它为父节点的右子节点,否则为左子节点。 + + /** + * 添加数据 + * @param data + */ + public void add(Object data){ + //判断是否有节点包含data数据 + if (contains(data)) { + //直接返回 + return; + }else { + //否则创建节点,将数据放入节点中 + Node node = new Node(data); + //找父节点 + if (root == null) { + //说明二叉树为空 + root = node; + }else { + //二叉树不为空 + Node parent = findParent(data); + //设置为新节点的父节点 + node.parent = parent; + //比较数据data和parent的数据大小 + if (compare(data, parent.data) > 0) { + //如果比parent大,则放到右边 + parent.right = node; + }else { + parent.left = node; + } + } + } + } +***查找data所在的节点*** + +* 从根节点开始遍历。 +* while(node != null) +* 如果node.data.equals(data) &&node.data.hashCode() == data.hashCode()则找到了,break退出循环。 +* 如果没找到,则比较data和node.data的大小,则从右边找,否则从左边找。 + + /** + * 查找数据所在节点 + * @param data + */ + private Node findNode(Object data) { + //从根节点开始遍历 + Node node = root; + while(node != null){ + //比较node.data和data + if (node.data.equals(data) && node.data.hashCode() == data.hashCode()) { + //找到数据了,直接break + break; + }else { + //否则,比较两个数据的大小,看看是往左还是往右找 + if (compare(data,node.data) > 0){ + //说明data比节点数据大,则往右找 + node = node.right; + }else { + node = node.left; + } + } + } + return node; + } +***查找新增数据的父节点*** + +* 从根节点开始遍历。 +* 定义一个parent节点 +* while(node != null) +* parent = node; +* 比较当前节点数据和新增数据的大小,如果新增数据大则往右找,否则往左找。 +* 如果node为空了,那么上一次记录的parent就是父节点。 + + /** + * 找父节点 + * @param data + * @return + */ + private Node findParent(Object data) { + //从根节点开始遍历 + Node node = root; + Node parent = null; + while(node != null){ + //每次记录可能的父节点 + parent = node; + //比较数据的大小 + if (compare(data,node.data) > 0) { + //如果data比节点的数据大 + //往右找 + node = node.right; + }else { + //否则往左找 + node = node.left; + } + } + return parent; + } +***遍历察看所有的数据*** + +递归 + privete void see(Node node){ + + if(node != null){ + see(node.left); + system.out.println(node.data); + see(node.right); + + } + +**删除数据** + +***删除根节点*** + +* 没有子节点 +* 只有左节点 +* 只有右节点 +* 两个子节点 + +***非根节点*** + +* 没有子节点 +* 只有左节点 +* 只有右节点 +* 两个子节点 + +***删除数据的步骤:*** + +* 查找数据所在的节点 +* 删除数据 + + /** + * 删除数据 + * + ***删除根节点*** + * 没有子节点 + * 只有左节点 + * 只有右节点 + * 两个子节点 + ***非根节点*** + * 没有子节点 + * 只有左节点 + * 只有右节点 + * 两个子节点 + * @param data + */ + public void delete(Object data){ + //首先查找数据所在节点 + Node node = findNode(data); + if (node != null) { + //如果节点存在 + //1.是根节点 + if (node == root) { + //1.1没有子节点 + if (node.left == null && node.right == null) { + //直接将根节点置为空即可 + root = null; + }else if (node.left == null) { + //只有右儿子 + //root指向右儿子,右儿子继位 + root = root.right; + //父皇驾崩,右儿子没有爹了 + root.parent = null; + }else if (node.right == null) { + //只有左二子 + //左二子继位 + root = root.left; + root.parent = null; + }else { + //有两个儿子 + //规定左二子继位 + //分裂两个子节点,让保留左儿子,让右儿子放到左二子的最右边 + Node left = split(node); + //将左二子设为根节点 + root = left; + root.parent = null; + } + }else { + //如果不是根节点 + if (node.left == null && node.right == null) { + //如果没有子节点 + //看该节点是左儿子还是右儿子 + if (compare(node.data, node.parent.data) > 0) { + //右儿子 + node.parent.right = null; + }else { + //左儿子 + node.parent.left = null; + } + }else if (node.left == null) { + //如果只有右儿子 + //爷爷变父亲 + node.right.parent = node.parent; + //比较要删除的节点与父节点的大小,确定要当做左儿子还是右儿子 + if (compare(node.data, node.parent.data) > 0) { + //如果比父节点大,则当成右节点 + node.parent.right = node.right; + }else { + node.parent.left = node.left; + } + }else if (node.right == null) { + //如果只有左儿子 + //爷爷变父亲 + node.left.parent = node.parent; + //比较要删除的节点与父节点的大小,确定要做左儿子还是右儿子 + if (compare(node.data, node.parent.data) > 0) { + //如果node大,则是右儿子 + node.parent.right = node.left; + }else { + //左儿子 + node.parent.left = node.left; + } + }else { + //如果两边都有儿子 + //先分裂节点 + Node left = split(node); + //得到了左儿子 + //左儿子继位 + left.parent = node.parent; + //比较node和node的父节点的 + if (compare(node.data, node.parent.data) > 0){ + //右儿子 + node.parent.right = left; + }else { + //左儿子 + node.parent.left = left; + } + } + } + } + } + +***分裂一个节点的两个儿子,保留左儿子,将右儿子放到左儿子的最右边*** + + /** + * 分裂一个节点的两个子节点,保留左儿子,将右儿子放到左儿子的最右边 + * @param node + */ + private Node split(Node node) { + //得到左儿子 + Node left = node.left; + //给右儿子找父节点,从left开始找 + Node parent = findParent(left, node.right.data); + //将parent设为右儿子的父节点 + node.right.parent = parent; + //parent的右儿子是node.right + parent.right = node.right; + return left; + } \ No newline at end of file