算法基础
一、什么是算法与大O表示法
算法是一组完成任务的指令。任何代码片段都可视为算法。
算法是一种通过有限过程解决问题的解决方案。
大O 表示法:
大O表示法是一种特殊的表示法,指出了算法的速度有多快;
大O表示法让你能够比较操作数,它指出了算法运行时间的增速;
大O 表示法指出了最糟情况下的运行时间。
举例,假设列表包含n个元素:
简单查找需要检查每个元素,因此需要执行n次操作。使用大O表示法,这个运行时间为 O(n)。
二分查找需要执行log n次操作。使用大O表示法,这个运行时间为 O(log n)。
算法相关数据结构
1、数组
数组中的元素在内存中都是相连的(紧靠在一起的)。当增加元素时,要请求计算机重新分配一块可容纳所有元素的内存,再将所有元素都移到那里。
需要随机地读取元素时,数组的效率很高,因为可迅速找到数组的任何元素。数组随机读取某个元素的时间复杂度为O(1)。
在同一个数组中,所有元素的类型都必须相同(都为int、double等)。
// 数组中的元素类型必须相同
int[] num={1,2,3,4,5};
Stirng[] strs={"a","b","c","d"};
数组:
插入:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fELiKyzE-1645899170072)(images/1.gif)]
删除:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5dqemf7D-1645899170072)(images/2.gif)]
2、链表
链表中的元素可存储在内存的任何地方;链表的每个元素都存储了下一个元素的地址,从而使一系列随机的内存地址串在一起。
链表的问题:无法直接读取链表的最后一个元素,因为你不知道它所处的地址,必须先访问元素#1,依次下去;跳跃取值时,效率很低。
链表的优势在插入元素和删除元素方面。
使用链表时,插入元素很简单,只需修改它前面的那个元素指向的地址。
使用数组时,则必须将后面的元素都向后移;如果没有足够的空间,可能还得将整个数组复制到其他地方!
因此,当需要在中间插入元素时,链表是更好的选择。
要删除一个元素,链表也是更好的选择,因为只需修改前一个元素指向的地址即可。而使用数组时,删除元素后,必须将后面的元素都向前移
链表
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vp142EhL-1645899170073)(images/2.png)]
插入:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EXXyarwu-1645899170073)(images/3.gif)]
删除:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jBb1IAnU-1645899170073)(images/4.gif)]
3、栈,队列,双端队列
栈 stack :一个有序的项的集合。添加项和移除项发生在同一端,即后进先出,形式如“弹夹”“叠盘子”。
队列 queue:一系列有序的元素的集合。先进先出,形式如“排队”。
双端队列 deque:一系列有序的元素的集合。允许从两端插入和从两端删除。
####栈
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0gYDyc4n-1645899170073)(images/3.png)]
入栈:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J5Vbfxrk-1645899170074)(images/5.gif)]
出栈:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FbAD9iW2-1645899170074)(images/6.gif)]
队列
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PIiIV6O0-1645899170074)(images/4.png)]
进队:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2pKxtFY8-1645899170074)(images/7.gif)]
出队:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gOT56CcG-1645899170074)(images/8.gif)]
位运算
我们常用的 3
, 5
等数字是十进制表示,而位运算的基础是二进制。即人类采用十进制,机器采用的是二进制,要深入了解位运算,就需要了解十进制和二进制的转换方法和对应关系。
二进制
十进制转二进制时,采用 “除 2 取余,逆序排列” 法:
- 用 2 整除十进制数,得到商和余数;
- 再用 2 整除商,得到新的商和余数;
- 重复第 1 和第 2 步,直到商为 0;
- 将先得到的余数作为二进制数的高位,后得到的余数作为二进制数的低位,依次排序;
排序结果就是该十进制数的二进制表示。例如十进制数 101
转换为二进制数的计算过程如下:
101 % 2 = 50 余 1
50 % 2 = 25 余 0
25 % 2 = 12 余 1
12 % 2 = 6 余 0
6 % 2 = 3 余 0
3 % 2 = 1 余 1
1 % 2 = 0 余 1
逆序排列即二进制中的从高位到低位排序,得到 7
位二进制数为 1100101
,如果要转换为 8
位二进制数,就需要在最高位补 0
。即十进制数的 8
位二进制数为 01100101
位运算概述
从现代计算机中所有的数据二进制的形式存储在设备中。即 0、1 两种状态,计算机对二进制数据进行的运算(+、-、*、/)都是叫位运算,即将符号位共同参与运算的运算。 程序中的所有数在计算机内存中都是以二进制的形式储存的。位运算说穿了,就是直接对整数在内存中的二进制位进行操作。
符号 | 描述 | 运算规则 |
---|---|---|
& | 与 | 两个位都为1时,结果才为1 |
| | 或 | 两个位都为0时,结果才为0 |
^ | 异或 | 两个位相同为0,相异为1 |
~ | 取反 | 0变1,1变0 |
<< | 左移 | 各二进位全部左移若干位,高位丢弃,低位补0 |
>> | 右移 | 各二进位全部右移若干位,对无符号数,高位补0,有符号数,各编译器处理方法不一样,有的补符号位(算术右移),有的补0(逻辑右移) |
按位与运算符 (&)
定义:参加运算的两个数据,按二进制位进行"与"运算。
运算规则:
0&0=0 0&1=0 1&0=0 1&1=1
总结:两位同时为1,结果才为1,否则结果为0。
例如:3&5 即 0000 0011& 0000 0101 = 0000 0001,因此 3&5 的值得1。
注意:负数按补码形式参加按位与运算。
与运算的用途:
1)清零
如果想将一个单元清零,即使其全部二进制位为0,只要与一个各位都为零的数值相与,结果为零。
2)取一个数的指定位
比如取数 X=1010 1110 的低4位,只需要另找一个数Y,令Y的低4位为1,其余位为0,即Y=0000 1111,然后将X与Y进行按位与运算(X&Y=0000 1110)即可得到X的指定位。
3)判断奇偶
只要根据最未位是0还是1来决定,为0就是偶数,为1就是奇数。因此可以用if ((a & 1) == 0)代替if (a % 2 == 0)来判断a是不是偶数。
按位或运算符(|)
定义:参加运算的两个对象,按二进制位进行"或"运算。
运算规则:
0|0=0 0|1=1 1|0=1 1|1=1
总结:参加运算的两个对象只要有一个为1,其值为1。
例如:3|5即 0000 0011| 0000 0101 = 0000 0111,因此,3|5的值得7。
注意:负数按补码形式参加按位或运算。
或运算的用途:
1)常用来对一个数据的某些位设置为1
比如将数 X=1010 1110 的低4位设置为1,只需要另找一个数Y,令Y的低4位为1,其余位为0,即Y=0000 1111,然后将X与Y进行按位或运算(X|Y=1010 1111)即可得到。
异或运算符(^)
定义:参加运算的两个数据,按二进制位进行"异或"运算。
运算规则:
0^0=0 0^1=1 1^0=1 1^1=0
总结:参加运算的两个对象,如果两个相应位相同为0,相异为1。
异或的几条性质:
- 1、交换律
- 2、结合律 (ab)c == a(bc)
- 3、对于任何数x,都有 xx=0,x0=x
- 4、自反性: abb=a^0=a;
异或运算的用途:
1)翻转指定位
比如将数 X=1010 1110 的低4位进行翻转,只需要另找一个数Y,令Y的低4位为1,其余位为0,即Y=0000 1111,然后将X与Y进行异或运算(X^Y=1010 0001)即可得到。
2)与0相异或值不变
例如:1010 1110 ^ 0000 0000 = 1010 1110
3)交换两个数
void Swap(int &a, int &b){
if (a != b){
a ^= b;
b ^= a;
a ^= b;
}
}
取反运算符 (~)
定义:参加运算的一个数据,按二进制进行"取反"运算。
运算规则:
~1=0
~0=1
总结:对一个二进制数按位取反,即将0变1,1变0。
异或运算的用途:
1)使一个数的最低位为零
使a的最低位为0,可以表示为:a & 1。1的值为 1111 1111 1111 1110,再按"与"运算,最低位一定为0。因为" ~"运算符的优先级比算术运算符、关系运算符、逻辑运算符和其他运算符都高。
左移运算符(<<)
定义:将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。
设 a=1010 1110,a = a<< 2 将a的二进制位左移2位、右补0,即得a=1011 1000。
若左移时舍弃的高位不包含1,则每左移一位,相当于该数乘以2。
右移运算符(>>)
定义:将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。
例如:a=a>>2 将a的二进制位右移2位,左补0 或者 左补1得看被移数是正还是负。
操作数每右移一位,相当于该数除以2。
复合赋值运算符
位运算符与赋值运算符结合,组成新的复合赋值运算符,它们是:
&= 例:a&=b 相当于 a=a&b
|= 例:a|=b 相当于 a=a|b
>>= 例:a>>=b 相当于 a=a>>b
<<= 例:a<<=b 相当于 a=a<<b
^= 例:a^=b 相当于 a=a^b
运算规则:和前面讲的复合赋值运算符的运算规则相似。
不同长度的数据进行位运算:如果两个不同长度的数据进行位运算时,系统会将二者按右端对齐,然后进行位运算。
以"与运算"为例说明如下:我们知道在C语言中long型占4个字节,int型占2个字节,如果一个long型数据与一个int型数据进行"与运算",右端对齐后,左边不足的位依下面三种情况补足,
-
1)如果整型数据为正数,左边补16个0。
-
2)如果整型数据为负数,左边补16个1。
-
3)如果整形数据为无符号数,左边也补16个0。
-
如:long a=123;int b=1;计算a& b。
-
如:long a=123;int b=-1;计算a& b。
-
如:long a=123;unsigned intb=1;计算a & b。
排序算法
选择排序
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
1. 算法步骤
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
重复第二步,直到所有元素均排序完毕。
2. 动图演示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T3kzE7XS-1645899170074)(images/9.gif)]
3.代码实现
public class SelectionSort implements IArraySort {
@Override
public int[] sort(int[] sourceArray) throws Exception {
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
// 总共要经过 N-1 轮比较
for (int i = 0; i < arr.length - 1; i++) {
int min = i;
// 每轮需要比较的次数 N-i
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[min]) {
// 记录目前能找到的最小值元素的下标
min = j;
}
}
// 将找到的最小值和i位置所在的值进行交换
if (i != min) {
int tmp = arr[i];
arr[i] = arr[min];
arr[min] = tmp;
}
}
return arr;
}
}
冒泡排序
冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。
作为最简单的排序算法之一,冒泡排序给我的感觉就像 Abandon 在单词书里出现的感觉一样,每次都在第一页第一位,所以最熟悉。冒泡排序还有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来
说并没有什么太大作用。
1. 算法步骤
比较相邻的元素。如果第一个比第二个大,就交换他们两个。
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
2. 动图演示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gEjnuQZW-1645899170075)(images/10.gif)]
3.什么时候最快
当输入的数据已经是正序时(都已经是正序了,我还要你冒泡排序有何用啊)。
4.什么时候最慢
当输入的数据是反序时(写一个 for 循环反序输出数据不就行了,干嘛要用你冒泡排序呢,我是闲的吗)。
5.代码实现
public class BubbleSort implements IArraySort {
@Override
public int[] sort(int[] sourceArray) throws Exception {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
for (int i = 1; i < arr.length; i++) {
// 设定一个标记,若为true,则表示此次循环没有进行交换,也就是待排序列已经有序,排序已经完成。
boolean flag = true;
for (int j = 0; j < arr.length - i; j++) {
if (arr[j] > arr[j + 1]) {
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
flag = false;
}
}
if (flag) {
break;
}
}
return arr;
}
}
插入排序
插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。
1. 算法步骤
将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
2. 动图演示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o53iXJYb-1645899170075)(images/11.gif)]
3.代码实现
public class InsertSort implements IArraySort {
@Override
public int[] sort(int[] sourceArray) throws Exception {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
// 从下标为1的元素开始选择合适的位置插入,因为下标为0的只有一个元素,默认是有序的
for (int i = 1; i < arr.length; i++) {
// 记录要插入的数据
int tmp = arr[i];
// 从已经排序的序列最右边的开始比较,找到比其小的数
int j = i;
while (j > 0 && tmp < arr[j - 1]) {
arr[j] = arr[j - 1];
j--;
}
// 存在比其小的数,插入
if (j != i) {
arr[j] = tmp;
}
}
return arr;
}
}
for (int i = 1; i < arr.length; i++) {
// 记录要插入的数据
int tmp = arr[i];
// 从已经排序的序列最右边的开始比较,找到比其小的数
int j = i;
while (j > 0 && tmp < arr[j - 1]) {
arr[j] = arr[j - 1];
j--;
}
// 存在比其小的数,插入
if (j != i) {
arr[j] = tmp;
}
}
return arr;
}
}
持续更新中…