设计模式 - 策略模式

设计模式也是在工作中才会发现其作用。在规模较小的情况下,往往随意写来比较方便快速,可是对于大型的系统来说,各个部分用上合适的设计模式可以更好的维护系统,也更加的清晰。最近看了看一些web框架的源码,比如用的最多的Spring,一个web请求就会经过很多层的处理,其中用了很多设计模式,如果不了解则很难理解为什么要如此设计。所以还是从基础的开始吧。

下面说的是一个比较简单的 策略模式

策略模式的定义:一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。

不像其他模式的定义显得很绕,策略模式的含义一看就比较清晰了。行为或者算法在运行时可以更改,首先能想到的便是通过面向对象中的多态来达到目的。在运行的过程中,根据需要动态的设定对象,对于调用者来说只要持有他们共同的接口就行了。正好,策略模式也正是这样。

看下面的例子(来自《Head First 设计模式》):

对于一只鸭子,它有飞和叫的行为,但是不同的鸭子表现出来的不一样。如果在一个类中表示所有的鸭子,则要进行各种判断,所以使用策略模式,将能够根据需要表现出不同鸭子的性质。

首先定义它的飞的行为:

1
2
3
4
5
package io.lovs.learn.design.strategy;

public interface FlyBehavior {
void fly();
}

Read More

Git的基本操作使用

之前比较多的使用svn,现在主要使用git还有些不是很熟悉的地方,所以记录下基本的操作以及其含义,来更好的掌握这个优秀的版本控制工具。

创建本地仓库并同步到远端

1
2
3
4
5
6

git init
git add .
git commit -m "first commit"
git remote add origin https://github.com/mpetrel/test.git
git push -u origin master

按照上述的步骤完成讲本地仓库同步到远端的仓库当中,下面逐句解释其含义:

  1. 初始化本地仓库
  2. 添加当前目录下的所有修改文件,准备提交
  3. 将修改后的文件提交到本地仓库,-m后面的参数是注释说明
  4. 将本地仓库连接到远程仓库,并取别名为origin,在之后的pullpush操作将使用此别名

堆排序之索引堆

索引堆的优化之处

在前面的堆排序当中,学习了堆排序,并且基于数组创建了堆。但以数组作为二叉树创建的堆,在排序过程中直接移动了元素来完成排序,这样的操作有一下几个局限性:

  1. 排序时直接移动了元素,若元素本身的内容非常大,则这个操作将会比较费时间
  2. 元素的顺序发生了改变,则元素的索引与元素的关系将发生变化,若某些操作依赖由索引查找元素,则将无法完成

对于以上两点,可以在结构中插入一层索引,对元素的索引进行排序,这样就可以在不破坏原有元素的顺序下完成堆排序,索引堆即由此而来。

索引堆的实现

索引堆机遇原有的堆实现,代码如下:

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
149
150
151
152
153
//
// Created by madman on 2017/9/17.
// 索引堆
//
#include <cassert>
#include "iostream"
using namespace std;


//最大堆
class IndexMaxHeap{
private:
int* data;
int count;
int capacity;
int *indexes;

void shiftUp(int k){
while(k > 1 && data[indexes[k/2]] < data[indexes[k]]){
//swap(data[k/2], data[k]);
swap(indexes[k/2], indexes[k]);
k /= 2;
}
}

void shiftDown(int k){
//记录K处的值
int ok = data[indexes[k]];
//记录此处indexes的值
int startIndex = indexes[k];
//如果有子节点
while ( 2 * k <= count){
//设j为将要和k交换的位置
int j = 2 * k;
//如果有右孩子,先比较右孩子
if( j + 1 <= count && data[indexes[j+1]]>data[indexes[j]]){
//如果右孩子比左孩子大,则交换位置变为右孩子
j = j+1;
}

if(ok >= data[indexes[j]]){
break;
}

//swap(data[k], data[j]);
indexes[k] = indexes[j];
k = j;
}
indexes[k] = startIndex;
}

public:
IndexMaxHeap(int capacity){
//下标从1开始
data = new int[capacity + 1];
indexes = new int[capacity + 1];
count = 0;
this->capacity = capacity;
}

//堆构造方法2
IndexMaxHeap(int arr[], int capacity){
data = new int[capacity + 1];
for(int i=0;i<capacity;i++){
data[i+1] = arr[i];
}
count = capacity;
//对非叶子进行shiftDown操作
for(int i = capacity/2; i >= 1; i--){
shiftDown(i);
}

}

~IndexMaxHeap(){
delete[] data;
delete[] indexes;
}

int size(){
return count;
}

bool isEmpty(){
return count == 0;
}

void insert(int i, int item){
//从1位置开始放入的元素,此处要注意数组放满的情况
assert(count + 1 <= capacity);
assert(i + 1 >=1 && i + 1 <= capacity);
i += 1;
data[i] = item;
indexes[count + 1] = i;
count ++;
//维护元素位置
shiftUp(count);
}

//取出最大值,即跟节点
int extractMax(){
//堆中要有元素
assert(count>0);
int item = data[indexes[1]];

swap(indexes[count], indexes[1]);
count --;
shiftDown(1);
return item;
}

// 将 i 位置的元素改变为 item
void change(int i, int item){
i += 1;
cout << data[i] << "\n";
data[i] = item;

// 找到j使得 indexes[j] = i, j表示元素item在堆中的位置
for(int j = 1; j <= count; j++){
if (indexes[j] == i) {
shiftUp(j);
shiftDown(j);
return;
}

}

}


void printArr(){
for(int i=0;i<=count;i++){
cout<<data[indexes[i]]<<" ";
}
cout << "\n";
}
};

int main(){
int arr[] = {4,1,3,6,7,2,8,22,12,23,21,9,42,90};
IndexMaxHeap maxHeap = IndexMaxHeap(14);
// cout<<maxHeap.size();
for(int i=1;i<14;i++){
maxHeap.insert(i, arr[i]);
}

maxHeap.printArr();

maxHeap.change(13, 41);
cout << "\n";
maxHeap.printArr();

}

Read More

maven环境隔离

在maven实际使用过程中,可以对环境进行不同的配置隔离,方便线上,测试,本地环境的不同部署,避免了手动修改配置文件的麻烦,而且容易出错和遗漏。下面记录一下环境隔离的相关配置操作。

隔离配置及原理

新增resource节点

pom.xml文件的build节点中增加如下的配置

1
2
3
4
5
6
7
8
9
10
11
<resources>
<resource>
<directory>src/main/resources.${deploy.type}</directory>
<excludes>
<exclude>*.jsp</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>

该节点用于配制打包过程中的相关资源属性,此处使用${deploy.type}声明一个变量来指定不同的打包目录

新增profiles节点

pom.xml中新增profiles节点,配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

<profiles>
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<deploy.type>dev</deploy.type>
</properties>
</profile>
<profile>
<id>beta</id>
<properties>
<deploy.type>beta</deploy.type>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<deploy.type>prod</deploy.type>
</properties>
</profile>
</profiles>

Read More

n(logn)级别排序三 堆排序

在数据结构当中,对于先进先出的数据结构被称为队列,优先队列则是按照其优先级进行出队的操作。从队列的定义中可以看出,可以用一个数组来维护一个队列,其实现方式和出入队复杂度如下表所示

类型 入队 出队
普通数组 O(1) O(n)
顺序数组 O(n) O(1)
O(lgn) O(lgn)

可以看出,数组的两种方式各有优缺点,堆虽然入队出队都没有最快,但是整体上是比较优秀的。另外,堆可以动态的进行优先级的维护操作,因此下面就学习堆这种结构。

堆的结构和实现

堆的结构

堆的结构是树形的,其一种经典实现是二叉堆,对应的树形结构便是二叉树。这个堆满足以下几个特点:

  • 所有的子节点都不大于其父节点
  • 它是一颗完全二叉树(除最后一层外,每一层的节点个数必须是最大值;最后一层的节点必须集中在左侧)

满足上面的性质就构成了一个堆,这样的堆被称为最大堆,因为由上面的定义可以看出,在顶层的节点总是这个堆中最大的,同理最小堆则反过来。

堆的实现

树的结构一般使用链表的形式,比如二叉树,对于每个节点,我们会定义左右两个指针,分别指向它的左孩子和右孩子。但对于完全二叉树来说它满足这样一个性质:

如果对一个完全二叉树自顶向下,从左到右编号的话,每一个左节点的序号都是其父节点的两倍,右节点则是父节点两倍加一(索引从1开始)

因此我们可以根据这个规律用数组来存储一个堆。下面的代码演示了在C++中定义一个最大堆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//最大堆
class MaxHeap{
private:
int* data;
int count;

public:
MaxHeap(int capacity){
data = new int[capacity + 1];
count = 0;
}

~MaxHeap(){
delete data;
}

int size(){
return count;
}

bool isEmpty(){
return count == 0;
}
};

Read More

nlog(n)级别排序二 快速排序

快速排序原理

快速排序是另一个时间复杂度在nlog(n)级别的排序算法,它的基本思想是这样的:每次找出一个元素,把这个元素放到它有序时应该在的位置,操作完成后,再把该元素左边的元素和右边的元素进行同样的操作,直至整个数组排序完成。因此,在快速排序当中也适用了递归的方法。下面给出它的基础实现:

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
//
// Created by madman on 2017/7/25.
// 快速排序
//

#include "iostream"
using namespace std;

//对arr [l,r]区间进行partition操作,找出一个元素应该在的位置
int __partition(int arr[], int l, int r){
//要确定位置的元素,默认选择第一个
int v = arr[l];
//元素v排好序后应该在的位置
int j = l;
//从v后面一次考察每一个元素
for(int i=l+1;i<=r;i++){
if(arr[i]<v){
swap(arr[i], arr[j+1]);
j++;
}
}
//考察完毕,找到v应该在的位置,进行交换,并返回找到的位置
swap(arr[j], arr[l]);
return j;

}

//对arr中[l,r]区间进行快速排序
void __quickSort(int arr[], int l, int r){
if(l>=r)return;
//得到当前操作确定的位置,对该位置两边再次进行partition操作
int p = __partition(arr, l, r);
__quickSort(arr, l, p-1);
__quickSort(arr, p+1, r);

}

void quickSort(int arr[], int n){
__quickSort(arr, 0, n-1);
}

int main(){
int arr[] = {4,1,3,6,7,2,8,22,12,15,21,9,42,90};
//int arr[] = {4,1,3,6,1,2,2,22,3,15,21,9,42,90};
quickSort(arr, 14);
for(int i=0;i<14;i++){
cout<<arr[i]<<" ";
}
return 0;
}

Read More

nlog(n)级别排序一 归并排序

归并排序原理

归并排序采用分治的思想,将一个需要排序的序列不断进行二分,直到不可分割为止。此时每一组为一个元素,可视为有序,再向上进行归并,归并后再对每一组进行排序,再向上归并,直至归并为一个完整的有序序列。
其过程大概如下图所示:

归并排序过程(引自维基百科)

Read More

Java线程

基础信息

Java语言中有两种方式来创建一个线程:通过继承Thread类,或是实现Runnable接口,并实现其run()方法,通过得到的Thread实例调用start()方法,来启动一个线程。处理器将按照自己的策略来分配每个线程的执行,因此线程的执行顺序、时间等是不确定的,如果一个任务依赖于有序的执行,需要自行进行同步之类的操作。另外在创建了一个Thread后,在它的任务执行完毕之前,它将一直存在且垃圾回收器无法回收它。

线程的创建和销毁需要较大的开销,同时当显示的创建一个线程并启动它时,可能会由于代码中的缺陷,导致系统创建了大量的线程导致系统崩溃,在这里可以使用Executor来启动线程。Executor可以接收一个Thread或Runnable对象并启动它们,Executor常用的实现类型有CachedThreadPool, FiexdThreadPool,SingleThreadExecutor等,其中 FiexdThreadPool 线程池将创建一个固定大小的线程池,SingleThreadPool则创建单个线程的线程池;每一种类型的线程池都将在可能的情况下复用线程。

线程是没有返回值的,当一个线程启动过后,它将独立于启动它的线程;线程中的任何异常也无法被父线程捕捉到。因此一个线程启动后,要确保它能自行完成任务并处理期间发生的异常。如果需要执行后得到返回值,可以使用Callable来代替Runnable,实现其中的call()方法,并且,必须使用ExecutorService.submit()来调用它。使用实例如下:

Read More

基本排序算法

算法总是学学忘忘,不过在遇到一些深入点的问题不理解其背后的原理就比较尴尬了。所以打算重新拾起这一部分,从基础的开始去了解一些其他的算法。从第一堂课开始就是学的排序,因此也将排序作为一个开始吧。

几个基本的排序算法

选择排序

选择排序的思想是从列表中不断的找出最小(大)的值,来依次排列到列表左侧,当移动到最右端的时候排序就完成了。对于一个长度为N的数组,选择排序大约需要进行N²/2次比较合N次交换。

在最坏的情况下,每一个数都不再其应该在的位置上,则每个数都会发生交换。对于比较次数,比如恰好是一个逆序数组,则每次都需要比较到末尾才能找到最大(小)值,因此比较次数为(N-1)+(N-2)+…+2+1 = N(N-1)/2 约为N²/2

简单实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 选择排序
* @param a
*/
public static void selectionSort(int[] a){
int length = a.length;
for(int i=0;i<length;i++){
int min = i;
//内层循环负责找出后续元素最小的
for(int j=i;j<length;j++){
if(a[j]<a[min]){
min = j;
}
}
int temp = a[i];
a[i] = a[min];
a[min] = temp;
}
}

Read More