算法 - algorithm

算法 - 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实现回溯全排列

  1. 个人感觉完全理解回溯的话还是很难的,这里就简单的讲解一下回溯的原理以及代码实现,通过这个例子,对能够对回溯有个较为简单的理解。
  2. 在此之前,我们先来了解一下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 就会跟着一起变。这跟我们直觉上对「复制」的理解不同。
  1. 代码:
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. 参考文章
    Python-copy()与deepcopy()区别
    数组全排列算法的python实现

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;
        }
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值