如果你不想只作为一个业务逻辑coder的话,我建议来学学数据结构与算法吧,真的很有意思!
以下是我个人在学习过程中的笔记,如有错误的地方,可以指正。
数据结构就是一种存储数据的方式,通过不同的数据存储方式,已达到我们高效地访问和修改数据。
数据结构主要有:
后面会对这些数据结构作详细的说明!
1+1 =2 这个就是算法!
对于算法的理解就是利用一些规则的等到你想要的结果, 通过这些不同的规则来运算会有快和慢之分,也有空间占用上的多与少之分。这就是时间复杂度和空间复杂度。
举个例子:
LeetCode第一题——两数之和,如果我们按照传统的方式两遍循环那么时间会明显多于使用使用一遍循环加哈希表的方式,但是一遍循环加哈希表的方式会申请额外的内存空间,这里就有一个技巧:空间换时间。以后我们会通过LeetCode题目来感受算法的魅力有趣之处。
在介绍时间复杂度的时候,我们需要先了解一个概念——常数时间的操作
常数时间操作:一个操作的执行时间不以具体样本量为转移,每次执行时间都是固定时间。
比如:数组,我们想要找到一个位置的值,不管这个数组的元素有多少,或者位置如何变化,我们只需要指定下标就可以直接找到这个位置上的值,也就是说明,数组的元素多少不会影响找到一个位置上的值所花费的时间;与之相反的是链表,链表想要找到一个位置上的值,是需要循环遍历,如果链表中的值增多,那么我们相应的循环也可能变多,找到这个值的时间也会有所变化,这就是不是常数时间的操作。
执行时间固定的操作都是常数时间的操作;反之,执行时间不固定的操作,都不是常数时间的操作。
对于时间复杂度如何计算:举例:
比如我们对一个数组进行排序,使用选择排序的方式:
选择排序就是说:第一遍,拿第一个数和其他数比较,比较出一个最小的数,将他放在第一位;第二遍就是从第二位开始,拿它和第三位到最后一位数对比,找出最小值,将它放在第二位……这里每次找最小值都涉及到“找”(找到下一位数)、“比”(和这个值比较大小)、“赋值”三个常数时间操作。
假如数组的大小为n,从第一位开始,一次循环常数时间操作是n * (找+比)+1;第二位开始,一次循环常数时间操作是(n-1) * (找+比)+1……依次下去,我们将每次循环的常数时间操作相加,那么你会发现,这是一个等差数列的相加,并且得到2an² +bn+c简化为xn²+yn+z,最终我们根据最大阶数项可以得到,选择排序的时间复杂度为O(n²)
最终的时间复杂度,不看低阶项,也不看高阶项的系数——n无限大,低阶项和高阶项系数没有实际意义
对于这个为什么用O,可以不用过度在意,你将它理解为算法的渐进时间复杂度。
有关时间复杂度的介绍可以去看《漫画算法》中的介绍,这里就不搬运了。
我们在进行一些算法运算的过程中,申请了有限个变量(每申请一个变量,就对应了内存中的一片空间),申请的变量不以样本数变化,那么这个算法的空间复杂度就是O(1)。
但是,如果我们申请的变量会因为样本数量而变化,那么空间复杂度就是O(n)。
这需要注意的是,这里的空间复杂度是只,为了完成某个算法,需要申请的额外内存空间,而不是原本就需要开辟的空间,比如某个算法最终的结果就是需要你得到一个新的数组空间,那么这个新的数组空间就不能算作空间复杂度里面。
如果两个算法在时间复杂度上一样,那么这个时候我们就需要关注常数项时间了。
对于两个相同时间复杂度算法的常数项时间,只能通过大样本数据进行比较。其实在比较时间复杂度上,比较是一个非常重要的过程。
位运算:>>、<<、>>>、|、&、^
比如数字4,它的二进制是:0……0000100,4>>2 = 1 ,意思就是向右移2位,左侧是它的符号位,如果左侧为1,则移动的时候要补上最左侧的符号位(我们知道,二进制左侧代表正负数,0是正数,1是负数)。如果最左侧为0,则移动后左侧补上0,如果最左侧为1,则移动后左侧补上1,简单的来说,向右移动n位等于除以2^n。
与带符号右移区别就是移动后,左侧都是用0补上。
比如数字4,它的二进制是:0……0000100,4<<3 =32,意思就是向左移2位,右侧补上0,简单的来说,向左移动n位等于乘以2^n。
^:如果相对应位值相同,则结果为0,否则为1
比如数字20的二进为:010100;数字32的二进制为:100000;
那么 20 ^ 32 的二进制结果为110100,从而得到结果为52。
& :如果相对应位都是1,则结果为1,否则为0;
那么 20 & 32 的二进制结果为000000,从而得到结果为0。
| :如果相对应位都是 0,则结果为 0,否则为1;
那么 20 | 32 的二进制结果为110100,从而得到结果为52。
按位取反运算符翻转操作数的每一位,即0变成1,1变成0。
比如:~20 =-21,二进制为: 101011
package com.example.demo.algorithm;
/**
* @author: sunzhinan
* @create: 2020-08-08 13:03
* @description: java运算符介绍
*/
public class TestArithmetic {
/**
* 带符号右移
*/
public static void test1() {
int i = -4;
System.out.println("带符号右移 i : " + (i >> 2));
}
/**
* 不带符号右移
*/
public static void test2() {
int i = -4;
int j = 4;
System.out.println("不带符号右移 i : " + (i >>> 2));
System.out.println("不带符号右移 j : " + (j >>> 2));
}
/**
* 左移
*/
public static void test3() {
int i = 4;
System.out.println("左移 i :" + (i << 2));
}
/**
* 异或运算
*/
public static void test4() {
int i = 20;
int j = 32;
System.out.println("异或运算20^32 结果为 :" + (i^j));
}
/**
* & 运算
*/
public static void test5() {
int i = 20;
int j = 32;
System.out.println("20&32 结果为 :" + (i&j));
}
/**
* | 运算
*/
public static void test6() {
int i = 20;
int j = 32;
System.out.println("20|32 结果为 :" + (i|j));
}
/**
* 取反
*/
public static void test7() {
int i = 20;
System.out.println("20取反 结果为 :" + (~i));
}
public static void main(String[] args) {
test1();
test2();
test3();
test4();
test5();
test6();
test7();
}
}
好了上面的示例介绍了位运算的规则,那么接下来用几个例子来展示一下位运算的巧妙运用的例子:
在看这些例子的时候需要弄清楚 ^ 运算的一些特性:
n^n = 0;
0^n = n;
n^ m 等于 m ^ n;n^ m^ k等同于k^ n^ m;可以发现结果和^的位置无关,这就是交换律。
n^ m^ k 等同于 n^ ( m ^ k);
通过不定义额外变量来交互两个数的值;
package com.example.demo.algorithm;
/**
* @author: sunzhinan
* @create: 2020-08-08 13:42
* @description: 通过不定义额外变量来交互两个数的值
*/
public class TestExchange {
public static void main(String[] args) {
int a = 4;
int b = 6;
//这里只要记住:a^b^a = b; a^b^b = a;所以我们得到a^b后,只要将这个数分别^a或者^b,就可以的a和b
a = a^b;
b = a^b;
a = a^b;
System.out.println("a的值为" + a);
System.out.println("b的值为" + b);
}
}
在实际使用过程中,可以通过这种方式交互两个数字(需要注意的是:同一个内存的值交互为0;值相同,不同内存可以相互交换)
一个数组中,只有一个值出现了奇数次,其他都是出现偶数次,找出这个数。
例如:{2,5,6,5,5,6,2},这个出现奇数次的就是:5
package com.example.demo.algorithm;
/**
* @author: sunzhinan
* @create: 2020-08-08 13:58
* @description: 一个数组中,只有一个值出现了奇数次,其他都是出现偶数次,找出这个数
*/
public class TestOddNumber {
public static int[] arr = {2,5,6,5,5,6,2};
public static void main(String[] args) {
int k = 0;
for (int i = 0; i < arr.length; i++) {
//我们这边拆解开来就是:0^2^5^6^5^5^6^2;根据结合律得到:0^2^2^6^6^5^5^5;再细化0^(2^2)^(6^6)^(5^5)^5
//根据n^n = 0 和 0^n = n 可以得到 0^5,所以最后得到的这个值就是那个出现奇数次的值
k = k ^ arr[i] ;
}
System.out.println(k);
}
}
如何把一个int类型的数,提取出它二进制最右侧的1来
例如:int数为6584,它的二进制为:1100110111000 提取的结果为:0000000001000 也就是8。
package com.example.demo.algorithm;
/**
* @author: sunzhinan
* @create: 2020-08-08 14:14
* @description: 如何把一个int类型的数,提取出它二进制最右侧的1来
*/
public class Test01 {
public static void main(String[] args) {
int i = 6584;
/**
* 6584 : 1100110111000
* 首先取反得到:0011001000111
* 将这个值加1得到:0011001001000
* 再 & 运算得到:0000000001000
*/
int j = i&(~i+1);
System.out.println(j);
}
}
一个数组中,有两个值出现了奇数次,其他都是出现偶数次,找出这两个数
package com.example.demo.algorithm;
/**
* @author: sunzhinan
* @create: 2020-08-08 14:27
* @description:一个数组中,有两个值出现了奇数次,其他都是出现偶数次,找出这两个数
*/
public class TestTwoNumber {
public static int[] arr = {2,5,6,5,5,6,2,3};
public static void main(String[] args) {
int k = 0;
for (int i = 0; i < arr.length; i++) {
k = k ^ arr[i] ;
}
//首先得到的k1是这两个数^的结果
System.out.println(k);
//取这个值的原因是在下一次循环的时候判别两个出现奇数次得到数
//因为那两个出现奇数次的数,一定在这个数最右边出现1的位置上不同
int m = k&(~k + 1);
int n = 0;
for (int i = 0; i < arr.length; i++) {
if ((arr[i] & m )== 0) {
n = n^arr[i];
}
}
System.out.println("这两个值为 : " + n + " 与 " + (n^k));
}
}
看完上面的例子后思考一个问题?
证明:
(A & B )^ (A|B)= A ^ B