算法 - algorithm
1、基础算法
1.1 使用蛇形给二维数组赋值 - 矩形
效果:
分析:
首先从数组的最后一列开始给二维数组赋值,即列不变,行增加。再从最后一列向右边依次给元素赋值,等到给左边的最后一个元素赋值后在向上。再从一个行第一列依次给右边的元素赋值。
关键点:我们在这个过程中要判断当前行标与列表的位置,防止出现越界的现象。
代码如下:
import java.util.Scanner;
public class lesson01 {
public static void main(String[] args) {
materialMethod();
}
public static void materialMethod(){
Scanner scanner = new Scanner(System.in);
System.out.println("输入行宽与列宽,中间使用空格隔开:");
int h = scanner.nextInt(); // 设置行宽
int l = scanner.nextInt(); // 设置列宽
// 计数器
int count = 1;
int[][] a = new int[h][l]; // 动态定义二维数组
// 初始化数组
for (int i = 0; i < h; ++i){
for (int j = 0; j < l; ++j){
a[i][j] = 0; // 默认使二维数组元素的值全部为零
}
}
int x = -1; // 行标 // 5
int y = l - 1; // 列标 // 6
// 蛇形赋值
while (count < h * l){
/*
* x < h-1 判断行标是否越界
* a[x+1][y] == 0 判断下一个元素是否为零,即是否已经填充。
* 注意点:在以上的表达式a[x+1][y]中,x+1由于并没有将其运算结果赋值给x,所以x的值依然为初始值:-1
* */
while (x < h-1 && a[x+1][y] == 0){ // 向下
x++;
a[x][y] = count; // 以5行6列为例,第一个元素为a[0][5] = 1...
count++; // 每执行完一次,计数器加一
}
/*
* y > 0 判断列标是否越界
* a[x][y-1] == 0左边的元素是否为零,即是否已经填充
* */
while (y > 0 && a[x][y-1] == 0){ // 向左
y--;
a[x][y] = count;
count++;
}
/*
* x > 0 判断行标是否越界
* a[x-1][y] == 0上一个元素的值是否为零,即是否已经填充
* */
while (x > 0 && a[x-1][y] == 0){ // 向上
x--;
a[x][y] = count;
count++;
}
/*
* y < l - 1 判断列标是否越界,即是否已经填充
* a[x][y+1] == 0 判断右边的元素是否为零,即是否已经填充
* */
while (y < l - 1 && a[x][y+1] == 0){ // 向右
y++;
a[x][y] = count;
count++;
}
}
// 输出数组
for (int i = 0; i < h; ++i){
for (int j = 0; j < l; ++j){
System.out.printf("%5d", a[i][j]);
}
System.out.println();
}
}
以上代码块出现了很多重复性的代码,导致整个程序看起来相当的臃肿。
接下来优化代码,可以发现我们打印的蛇形是顺时针的,如果我们想要打印逆时针时,又不得不去更改原来的代码。为了解决这个问题,我们使用另一种方法来实现,关键代码块如下:
// 关键优化点
int[][] dir = new int[][]{{0,-1},{1,0},{0,1},{-1,0}}; // 逆时针
int k = 0;
while (count <= h*l){
a[x][y] = count;
count++;
int tx = x + dir[k][0];
int ty = y + dir[k][1];
/*
* tx>h-1 || tx < 0 判断是否越界
* ty > l - 1 || ty < 0
* */
if (tx > h-1 || tx < 0 || ty > l - 1 || ty < 0 || a[tx][ty] != 0){
k = (k+1)%4;
}
x = x + dir[k][0];
y = y + dir[k][1];
}
这样代码会变得很简洁,但是难度也提升了不少,代码行:
int[][] dir = new int[][]{{0,-1},{1,0},{0,1},{-1,0}};
分析的来源如下:
1.2 二分法查找
使用二分法查找可以大大的提高代码执行的效率,一般来说,对于包含n个元素的列表,使用二分法查找最多需要log2n(即以2为底,n的对数),而简单查找则最多需要n步!
可以使用Python来计算一个数的对数,代码如下:
# 其中语法为log(数,基数)
import math
math.log(100,2)
注意点,使用二分法前,必须对其列表的元素进行排序。
具体算法实现如下:
def binary_search(list, index):
low = 0
high = len(list)-1
while low <= high:
mid = (low + high)
guess = list[mid]
if guess == item:
return mid
if guess > item:
high = mid - 1
else:
low = mid + 1
return None
1.3链表
链表中的元素可以存储在内存中的任何地方,链表中的每个元素都存储了下一个元素的地址,从而使一系列随机的内存地址串在一起。
在链表中添加元素很容易:只需要将其放入内存,并将其地址存储到前一个元素中。
使用链表时,根本就不需要移动元素。链表的优势在于插入元素方面。
1.4 数组
使用链表存储数据,在其读取数据方面链表的效率很高;但如果需要跳跃,链表的效率真的很低。
数组就可以很好的解决这个问题,列如:一个包含5个元素的数组的起始地址是00,因为数组中的元素在其内存中是连续的,经过简单的运算,第五个元素的地址为05,地址拿到了,那我们就可以直接取到里面的值。
1.5 Python实现回溯全排列
- 个人感觉完全理解回溯的话还是很难的,这里就简单的讲解一下回溯的原理以及代码实现,通过这个例子,对能够对回溯有个较为简单的理解。
- 在此之前,我们先来了解一下Python的两个函数copy()与deepcopy(),这两个函数都能够实现列表的复制,但是稍有点不同,下面我们来进行测试:
>>> import copy
>>> original_list = [1,2,3,4,[5,6]]
>>> t1=copy.copy(original_list)
>>> t2=copy.deepcopy(original_list)
>>> t1==t2
True
>>> t1 is t2
False
>>> original_list[4][0]='python'
>>> original_list
[1, 2, 3, 4, ['python', 6]]
>>> t1
[1, 2, 3, 4, ['python', 6]]
>>> t2
[1, 2, 3, 4, [5, 6]]
结论:
1. 我们寻常意义的复制就是深复制,即将被复制对象完全再复制一遍作为独立的新个体单独存在。所以改变原有被复制对象不会对已经复制出来的新对象产生影响。
2. 而浅复制并不会产生一个独立的对象单独存在,他只是将原有的数据块打上一个新标签,所以当其中一个标签被改变的时候,数据块就会发生变化,另一个标签也会随之改变。这就和我们寻常意义上的复制有所不同了。
对于简单的 object,用 shallow copy 和 deep copy 没区别
复杂的 object, 如 list 中套着 list 的情况,shallow copy 中的 子list,并未从原 object 真的「独立」出来。也就是说,如果你改变原 object 的子 list 中的一个元素,你的 copy 就会跟着一起变。这跟我们直觉上对「复制」的理解不同。
- 代码:
import copy
all = [1, 2, 3, 4]
result = []
def backtracking(remain_list, res_list): # remain_list 原始列表 #res_list 结果列表
if len(remain_list) == 1:
res_list.append(remain_list[0])
print(res_list)
result.append(res_list)
else:
for j in range(len(remain_list)):
remain_list_c = copy.deepcopy(remain_list)
res_list_c = copy.deepcopy(res_list)
twp = remain_list_c.pop(j)
res_list_c.append(twp)
backtracking(remain_list_c, res_list_c)
backtracking(all, [])
print(len(result))
1.6 Python 解迷宫(maze)
1. 问题描述:
输入n * m 的二维数组 表示一个迷宫,数字0表示障碍, 1表示能通行,移动到相邻单元格用1步,求解迷宫路径。
2. 问题分析:
基本思路是:
每个时刻总有一个当前位置,开始时这个位置是迷宫人口。
如果当前位置就是出口,问题已解决。
否则,如果从当前位置己无路可走,当前的探查失败,回退一步。
取一个可行相邻位置用同样方式探查,如果从那里可以找到通往出口的路径,那么从当前位置到出口的路径也就找到了。
在整个计算开始时,把迷宫的人口(序对)作为检查的当前位置,算法过程就是:
mark当前位置:
检查当前位置是否为出口,如果是则成功结束。
逐个检查当前位置的四邻是否可以通达出口(递归调用自身)。
如果对四邻的探索都失败,报告失败。
3. 完整代码
dirs = [(0, 1), (1, 0), (0, -1), (-1, 0)] # 当前位置四个方向的偏移量, 右->下->左->上
path = [] # 存找到的路径
def mark(maze, pos): # 给迷宫maze的位置pos标"2"表示“到过了”
maze[pos[0]][pos[1]] = 2
def passable(maze, pos): # 检查迷宫maze的位置pos是否可通行
return maze[pos[0]][pos[1]] == 0
def find_path(maze, pos, end):
mark(maze, pos)
if pos == end:
print(pos, end=" ") # 已到达出口,输出这个位置。成功结束
path.append(pos)
return True
for i in range(4): # 否则按四个方向顺序检查
nextp = pos[0] + dirs[i][0], pos[1] + dirs[i][1]
# 考虑下一个可能方向
if passable(maze, nextp): # 不可行的相邻位置不管
if find_path(maze, nextp, end): # 如果从nextp可达出口,输出这个位置,成功结束
print(pos, end=" ")
path.append(pos)
return True
return False
def see_path(maze, path): # 使寻找到的路径可视化
for i, p in enumerate(path):
if i == 0:
maze[p[0]][p[1]] = "E"
elif i == len(path) - 1:
maze[p[0]][p[1]] = "S"
else:
maze[p[0]][p[1]] = 3
print("\n")
for r in maze:
for c in r:
if c == 3:
print('\033[0;31m' + "*" + " " + '\033[0m', end="")
elif c == "S" or c == "E":
print('\033[0;34m' + c + " " + '\033[0m', end="")
elif c == 2:
print('\033[0;32m' + "#" + " " + '\033[0m', end="")
elif c == 1:
print('\033[0;;40m' + " " * 2 + '\033[0m', end="")
else:
print(" " * 2, end="")
print()
if __name__ == '__main__':
maze = [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1],
[1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1],
[1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1],
[1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1],
[1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1],
[1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1],
[1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1],
[1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1],
[1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1],
[1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]
start = (1, 1)
end = (10, 12)
find_path(maze, start, end)
see_path(maze, path)
参考文章:理解回溯算法——回溯算法的初学者指南
2、二叉树
2.1 遍历二叉树的几种方法
二叉树有多种遍历方法,有层次遍历法,深度优先遍历法,广度优先遍历法等。
这里涉及到二叉树的先序、中序、后序的递归和非递归遍历,都是使用Java编写的。
二叉树节点类:
class TreeNode{
int value;
TreeNode left;
TreeNode right;
public TreeNode(){
this.value = 0;
this.left = null;
this.right = null;
}
}
无论是哪种遍历方式,考查节点的顺序都是一样的,只不过有时候考查了节点,将其暂存,需要在之后的过程中输出。
先序:1 2 4 6 7 8 3 5
中序:4 7 6 8 2 1 3 5
后序:7 8 6 4 2 5 3 1
三种遍历方法的考查顺序一致,得到的结果却不一样,原因在于:
先序: 考察到一个节点后,立即输出该节点的值,并继续遍历其左右子树。
中序: 考查到一个节点后,将其暂存,遍历完左子树后,在输出该节点的值,然后遍历右子树。
后序: 考查到一个节点后,将其暂存,遍历完左右子树后,在输出该节点的值。
2.1.1 先序遍历
递归先序遍历很容易理解,先输出节点的值,再递归遍历左右子树。中序和后序的递归类似,改变根节点输出位置即可。
// 先序
public static void preOrder(TreeNode treeNode){
if (treeNode == null) return;
System.out.print(treeNode.value + " ");
preOrder(treeNode.left);
preOrder(treeNode.right);
}
2.1.2 非递归先序遍历
遍历过程参考注释:
// 非递归先序遍历
public static void preorderTraversal(TreeNode root) {
// 用来暂存节点的栈
Stack<TreeNode> treeNodeStack = new Stack<TreeNode>();
// 新建一个游标节点为根节点
TreeNode node = root;
// 当遍历到最后一个节点的时候,无论它的左右子树都为空,并且栈也为空
// 所以,只要不同时满足这两点,都需要进入循环
while (node != null || !treeNodeStack.isEmpty()) {
// 若当前考查节点非空,则输出该节点的值
// 由考查顺序得知,需要一直往左走
while (node != null) {
System.out.print(node.val + " ");
// 为了之后能找到该节点的右子树,暂存该节点
treeNodeStack.push(node);
node = node.left;
}
// 一直到左子树为空,则开始考虑右子树
// 如果栈已空,就不需要再考虑
// 弹出栈顶元素,将游标等于该节点的右子树
if (!treeNodeStack.isEmpty()) {
node = treeNodeStack.pop();
node = node.right;
}
}
}