Melon

Melon website

0%

二叉树的遍历采用递归实现是比较简单的一种方式,在实际使用中也常会采用递归实现。但了解如何采用非递归实现,更能加深对二叉树遍历的原理。

非递归的遍历实现,实际会使用到栈的结构。通过对树节点的入栈和出栈,实现二叉树的遍历。

以下为实现代码(Java):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
package com.algorithms;

import java.util.Stack;

public class BinaryTree {

/**
* 二叉树前序打印-递归实现
*
* @param node 根节点
*/
public static void preSearch(Node node) {
if (node == null) {
return;
}
System.out.print(node.val + " ");
preSearch(node.left);
preSearch(node.right);
}

/**
* 二叉树前序打印-遍历实现
*
* @param node 根节点
*/
public static void preSearchV2(Node node) {
Stack<Node> stack = new Stack<>();
stack.add(node);
while (!stack.empty()) {
Node n = stack.pop();
System.out.print(n.val + " ");
if (n.right != null) {
stack.add(n.right);
}
if (n.left != null) {
stack.add(n.left);
}
}
}

/**
* 二叉树中序打印-递归实现
*
* @param node 根节点
*/
public static void midSearch(Node node) {
if (node == null) {
return;
}
midSearch(node.left);
System.out.print(node.val + " ");
midSearch(node.right);
}

/**
* 二叉树中序打印-遍历实现
*
* @param node 根节点
*/
public static void midSearchV2(Node node) {
Stack<Node> stack = new Stack<>();
while (!stack.empty() || node != null) {
if (node != null) {
stack.push(node);
node = node.left;
} else {
node = stack.pop();
System.out.print(node.val + " ");
node = node.right;
}
}
}

/**
* 二叉树后序打印-递归实现
*
* @param node 根节点
*/
public static void postSearch(Node node) {
if (node == null) {
return;
}
postSearch(node.left);
postSearch(node.right);
System.out.print(node.val + " ");
}

/**
* 二叉树后序打印-遍历实现
*
* @param node 根节点
*/
public static void postSearchV2(Node node) {
if (node == null) {
return;
}
Stack<Node> stack = new Stack<>();
Node c;
stack.push(node);

while (!stack.empty()) {
c = stack.peek();
if (c.left != node && c.left != null && node != c.right) {
stack.push(c.left);
} else if (c.right != node && c.right != null) {
stack.push(c.right);
} else {
c = stack.pop();
System.out.print(c.val + " ");
node = c;
}
}
}

public static void main(String[] args) {
Node root = new Node(1);
Node n2 = new Node(2);
Node n3 = new Node(3);
root.left = n2;
root.right = n3;
Node n4 = new Node(4);
Node n5 = new Node(5);
n2.left = n4;
n2.right = n5;
Node n6 = new Node(6);
Node n7 = new Node(7);
n3.left = n6;
n3.right = n7;
Node n8 = new Node(8);
Node n9 = new Node(9);
n5.left = n8;
n5.right = n9;

preSearch(root);
System.out.println();
preSearchV2(root);
System.out.println();
System.out.println();
midSearch(root);
System.out.println();
midSearchV2(root);
System.out.println();
System.out.println();
postSearch(root);
System.out.println();
postSearchV2(root);
}
}

排序算法简单介绍

选择排序

不断地选择剩余元素之中的最小者

  • 找到数组中最小的那个元素,将它和数组的第一个元素交换位置(如果最小就是第一个元素,和自己交换)
  • 在剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置
  • 往复至将整个数组排好序

插入排序

插入排序所需时间取决于输入中元素的初始顺序。

  • 从索引1的位置开始第一个循环遍历
  • 从第一个循环遍历的当前索引往索引位置为0开始第二个循环遍历
  • 第二个循环中,如果j位置不小于j-1时,退出循环(前面位置数据都不大于j位置,所以前面是有序的)。否则,将j与j-1位置交换

冒泡排序

如名称冒泡一样,比较小的数会逐渐的冒出来。
重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。

  • 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  • 针对所有的元素重复以上的步骤,除了最后一个。
  • 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

希尔排序

是为了加快速度简单地改进了插入排序。
核心思想是使数组中任意间隔为h的元素都是有序的。

  • h计算方法:初始值1,在不大于数组长度/3的时候,取序列1/2(3^K-1)最大值
    1
    2
    3
    例:
    int h =1
    while (h < N/3) h = 3*h + 1;
  • 从索引h的位置开始第一个循环遍历
  • 从第一个循环遍历的当前索引往索引位置为h开始第二个循环遍历
  • 第二个循环中,如果j位置不小于j-h时,退出循环(前面位置数据都不大于j位置,所以前面是有序的)。否则,将j与j-h位置交换

归并排序

将两个有序的数组归并成一个更大的有序数组,从而使整个数组有序

归并方法

  • 在开始前需声明一个与待排序数组同样长度的数组aux
  • 将lo到hi所有元素复制到aux数组
  • 通过4个条件判断进行归并
    • 左半边用尽,取右半边元素
    • 右半边用尽,取左半边元素
    • 右半边的当前元素小于左半边的当前元素,取右半边元素
    • 右半边的当前元素大于等于左半边的当前元素,取左半边元素
  • 归并完成后会得到一个有序数组

自顶向下的归并排序

  • 取中间索引(int mid = lo + (hi - lo)/2;)
  • 对左半边递归排序
  • 对右半边递归排序
  • 归并左右有序数组

自底向上的归并排序

  • 从长度为1的数组开始两两归并,然后是四四归并,然后是八八归并,一直下去直到越界

快速排序

将一个数组分成两个子数组,将两部分独立地排序

  • 将数组元素顺序打乱
  • 将数组进行切分
    • 取切分数组第一个元素
    • 从左右边界遍历数组
    • 左侧,如果元素大于第一个元素,与右侧第一个小于第一个元素交换索引(小于放左边,大于放右边,相等保持不动)
    • 将第一个元素缩影与右侧数组右边界-1交换
  • 将切分后左侧数组继续切分
  • 将切分后右侧数组继续切分

在对小数组的快速排序可以切换到插入排序

快速排序(三向切分)

对快速排序算法的改进,数组中有大量重复数据时表现好
将原先左右两部分换成,大于等于小于三个部分

  • 将数组元素顺序打乱
  • 定义指针lt = lo,i = lo + 1,gt = hi,v = a[lo]
  • 当i <= gt时,遍历
  • a[i] 小于v,交换lt与i的元素,将lt和i加一
  • a[i] 大于v,交换gt与i的元素,将gt减一
  • a[i] 等于v,将i加一
  • 递归左侧数组 lo, lt -1
  • 递归右侧数组 gt+1, hi
  • 中间数组(lt,gt)已经有序,不用再递归排序

堆排序

堆(heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。堆总是满足下列性质:

  • 堆中某个结点的值总是不大于或不小于其父结点的值;
  • 堆总是一棵完全二叉树。

将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。

插入元素:将新元素加到数组末尾,增加堆的大小并让这个新元素上浮到合适的位置
上浮:通过交换节点和它的父节点来实现上浮,如果节点比父节点大,进行交换

1
2
3
4
5
6
private void swim(int k){
while (k > 1 && less(k/2, k)){ // k/2即为父节点的索引
exch(k/2, k); // 交换
k = k/2; // 把索引换成父节点,并继续循环比较父节点与它的父节点
}
}

删除最大元素:从数组顶端删去最大的元素并将数组的最后一个元素放到顶端,减小堆的大小并让这个元素下沉到合适的位置
下沉:将节点向下一段直到它的子节点都比它更小或是到达了堆的底部

1
2
3
4
5
6
7
8
9
private void sink(int k){
while (2*k <= N){
int j = 2*k;
if (j < N && less(j, j+1)) j++; //使用子节点中比较大的那个
if (!less(k, j)) break; // 如果比子节点大,退出
exec(k, j); // 与子节点中比较大的那个交换
k = j; // 使用交换后的索引继续下沉
}
}

堆排序就是通过下沉排序完成
堆排序算法:

1
2
3
4
5
6
7
8
9
10
11
public static void sort(Comparable[] a){
int N = a.length;
for (int k = N/2; k >= 1; k--){ // 从拥有子节点的节点开始做sink操作,直到根节点
sink(a, k, N); // 将数组构造为堆
}

while(N>1){
exch(a, 1, N--); // 把根节点与堆最后一个节点进行交换
sink(a, 1, N); // 进行下沉排序
}
}

排序算法性能特点

稳定性:如果一个排序算法能够保证数组中重复元素的相对位置则可以被称为是稳定的。

原地排序:基本上不需要借助额外的数据结构就能对输入的数据进行变换的算法。不过,分配少量空间给部分辅助变量是被允许的。算法执行过程中,输入的数据往往会被输出结果覆盖。原地算法只能通过替换或交换元素的方式来修改原始的输入。

算法 是否稳定 是否为原地排序 时间复杂度 空间复杂度 备注
选择排序 N^2 1
插入排序 介于N和N^2之间 1 取决于输入元素的排列情况
希尔排序 NlogN?
N^6/5?
1
快速排序 NlogN lgN
三向快速排序 介于N和NlogN之间 lgN 运行效率由概率提供保证,同时也取决于输入元素的分布情况
归并排序 NlogN N
堆排序 NlogN 1

作为算法的基础,重复多少次阅读都不为过。本文只会挑一些必须记忆或者容易混淆的知识点记录。

原始数据类型

整形(int)、双精度实数类型(double)、布尔型(boolean)、字符型(char)

不常用的原始数据类型有:
64位整数(long)、16位整数、8位整数(byte)、32位单精度实数(float)

单精度与双精度浮点数

  • float:小数点后六位有效
  • double: 小数点后十五位有效

字符串

字符串是由一串字符(char类型的值)组成的。字符串是一个数据类型,但并不是原始数据类型。
字符串在现代编程语言中非常常用,甚至比char基础数据类型还常用,但内心要知道他是由什么实现来的

基础数据结构

数组

  • 优点:通过索引可以直接访问任意元素
  • 缺点:在初始化时就需要知道元素的数量(扩容不便)

链表

  • 优点:使用的空间大小和元素数量成正比
  • 缺点:需要通过引用访问任意元素(索引访问不便,从root节点引用遍历)

在我们的后续的算法中,基础的数据结构都离不开这两种基础数据结构。

重新开始审视自己的过去,时间一天一天过去。好像得到了些什么,又好像什么都没有得到。

下午阳光明媚,想着看看之前的生活。好像没办法从记忆中提取。当下大家的记忆可能都在互联网上。看了一下之前自己的微博,觉得那时候的表达欲怎么这么强。记录生活是挺好的一件事情,现在好像都失去了。

可能需要再找回来呢。

网站搭建起来大半年了,一共也没发几篇博客。甚是惭愧。

4月

在技术上:给个简单的计划。或者是阅读个开源库的源码然后,或是记录一些算法,写够10篇技术博客吧。

在生活上:管住嘴,迈开腿。

人总是需要进步的。

最后,人生警句:知善恶,致良知。
view

Golang 处理emoji表情无法存入mysql utf8 字段

通过代码把不符合规范的字符过滤掉
原理为遍历字符串字符,检查字符在utf8中的长度,因为mysql utf8 仅能处理最大3字节的字符

1
2
3
4
5
6
7
8
9
10
//FilterUtf8SizeGreaterThree mysql数据库utf8只能存储字节3的utf8数据,这里把不符合的字符过滤掉
func FilterUtf8SizeGreaterThree(content string) string {
bf := bytes.NewBufferString("")
for _, value := range content {
if size := utf8.RuneLen(value); size <= 3 {
bf.WriteRune(value)
}
}
return bf.String()
}

还有一种办法是直接把数据库表或对应字段换成utf8mb4

在处理kubernetes的ci自动滚动更新部署时,会使用到命令:

1
kubectl apply -f deployment.yaml

但执行完这条命令后,ci脚本会立即结束,因为kubernetes不会等待部署完成才返回结果。

所有以下脚本可以在执行完滚动更新后,等待执行结果,并在失败时进行回滚,放弃掉这次更新。

1
2
3
4
5
6
kubectl apply -f myapp.yaml
if ! kubectl rollout status deployment myapp; then
kubectl rollout undo deployment myapp
kubectl rollout status deployment myapp
exit 1
fi

kubectl rollout status deployment myapp 如果部署失败,此命令会以非零返回码退出,以指示失败。

GitHub多个账户时如何配置SSH密钥

在工作中,可能会有企业帐号,个人帐号等github帐号需要同时配置ssh key,以便能进行认证并成功拉取代码。本文将介绍如何进行相关配置。

如何生成ssh key 见github指南:https://docs.github.com/cn/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent

1
2
生成指定名称ssh key命令:
ssh-keygen -t rsa -f ~/.ssh/id_rsa.key2 -C "email"

在 ~/.ssh 目录下新建一个config文件,添加如下内容(其中Host和HostName填写git服务器的域名,IdentityFile指定私钥的路径)

1
2
3
4
5
6
7
8
9
10
11
# github1
Host github1.com
HostName github.com
PreferredAuthentications publickey
IdentityFile ~/.ssh/id_rsa

# github2
Host github2.com
HostName github.com
PreferredAuthentications publickey
IdentityFile ~/.ssh/id_rsa.key2

当然,在本地生成好之后要把ssh加到github帐号SSH配置中,这里不再赘述。

测试命令:

1
2
3
测试key1:ssh -T [email protected]
测试key2:ssh -T [email protected]
测试命令成功时会返回ssh key对应的帐号信息

在真实项目中使用:

1
2
3
例:[email protected]:watermelon-ping/watermelon-ping.github.io.git
github1中的项目真实使用:[email protected]:watermelon-ping/watermelon-ping.github.io.git
github2中的项目真实使用:[email protected]:watermelon-ping/watermelon-ping.github.io.git

有用的命令:

1
git remote set-url origin remote-url

综述:

通过域名别名实现对github的多账户控制

工作中会接触挺多的事情,但做完后不会做一些总结。所以萌发了做一个自己的博客网站的想法。

然后就有了这个网站。

可能不会更新很频繁,但仍希望每月至少有4篇博文会更新到网站上去。

从今天开始吧,会一直保持这个网站的运营。