zzm99


  • Home

  • Tags

  • Categories

  • Archives

  • Search

Java 修饰符

Posted on 2019-05-19 | In java , 基础知识 |
Words count in article: 2.2k | Reading time ≈ 8

Java 修饰符

Java语言提供了很多修饰符,主要分为以下两类:

  • 访问修饰符
  • 非访问修饰符
    修饰符用来定义类、方法或者变量,通常放在语句的最前端。我们通过下面的例子来说明:
    Read more »

Java 变量类型

Posted on 2019-05-19 | In java , 基础知识 |
Words count in article: 1.4k | Reading time ≈ 4

Java 变量类型

Java语言支持的变量类型有:

  1. 类变量:独立于方法之外的变量,用 static 修饰。
  2. 实例变量:独立于方法之外的变量,不过没有 static 修饰。
  3. 局部变量:类的方法中的变量。
    Read more »

Java 基本数据类型

Posted on 2019-05-19 | In java , 基础知识 |
Words count in article: 2.7k | Reading time ≈ 11

Java 基本数据类型

变量就是申请内存来存储值。也就是说,当创建变量的时候,需要在内存中申请空间。

内存管理系统根据变量的类型为变量分配存储空间,分配的空间只能用来储存该类型数据。

通过定义不同类型的变量,可以在内存中储存整数、小数或者字符。

Read more »

网络应用开发

Posted on 2019-05-18 | In python , python100天计划 |
Words count in article: 1.1k | Reading time ≈ 4

网络应用开发

发送电子邮件
在即时通信软件如此发达的今天,电子邮件仍然是互联网上使用最为广泛的应用之一,公司向应聘者发出录用通知、网站向用户发送一个激活账号的链接、银行向客户推广它们的理财产品等几乎都是通过电子邮件来完成的,而这些任务应该都是由程序自动完成的。

就像我们可以用HTTP(超文本传输协议)来访问一个网站一样,发送邮件要使用SMTP(简单邮件传输协议),SMTP也是一个建立在TCP(传输控制协议)提供的可靠数据传输服务的基础上的应用级协议,它规定了邮件的发送者如何跟发送邮件的服务器进行通信的细节,而Python中的smtplib模块将这些操作简化成了几个简单的函数。

下面的代码演示了如何在Python发送邮件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from smtplib import SMTP
from email.header import Header
from email.mime.text import MIMEText


def main():
# 请自行修改下面的邮件发送者和接收者
sender = 'abcdefg@126.com'
receivers = ['uvwxyz@qq.com', 'uvwxyz@126.com']
message = MIMEText('用Python发送邮件的示例代码.', 'plain', 'utf-8')
message['From'] = Header('王大锤', 'utf-8')
message['To'] = Header('骆昊', 'utf-8')
message['Subject'] = Header('示例代码实验邮件', 'utf-8')
smtper = SMTP('smtp.126.com')
# 请自行修改下面的登录口令
smtper.login(sender, 'secretpass')
smtper.sendmail(sender, receivers, message.as_string())
print('邮件发送完成!')


if __name__ == '__main__':
main()

如果要发送带有附件的邮件,那么可以按照下面的方式进行操作。

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
from smtplib import SMTP
from email.header import Header
from email.mime.text import MIMEText
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart

import urllib


def main():
# 创建一个带附件的邮件消息对象
message = MIMEMultipart()

# 创建文本内容
text_content = MIMEText('附件中有本月数据请查收', 'plain', 'utf-8')
message['Subject'] = Header('本月数据', 'utf-8')
# 将文本内容添加到邮件消息对象中
message.attach(text_content)

# 读取文件并将文件作为附件添加到邮件消息对象中
with open('/Users/Hao/Desktop/hello.txt', 'rb') as f:
txt = MIMEText(f.read(), 'base64', 'utf-8')
txt['Content-Type'] = 'text/plain'
txt['Content-Disposition'] = 'attachment; filename=hello.txt'
message.attach(txt)
# 读取文件并将文件作为附件添加到邮件消息对象中
with open('/Users/Hao/Desktop/汇总数据.xlsx', 'rb') as f:
xls = MIMEText(f.read(), 'base64', 'utf-8')
xls['Content-Type'] = 'application/vnd.ms-excel'
xls['Content-Disposition'] = 'attachment; filename=month-data.xlsx'
message.attach(xls)

# 创建SMTP对象
smtper = SMTP('smtp.126.com')
# 开启安全连接
# smtper.starttls()
sender = 'abcdefg@126.com'
receivers = ['uvwxyz@qq.com']
# 登录到SMTP服务器
# 请注意此处不是使用密码而是邮件客户端授权码进行登录
# 对此有疑问的读者可以联系自己使用的邮件服务器客服
smtper.login(sender, 'secretpass')
# 发送邮件
smtper.sendmail(sender, receivers, message.as_string())
# 与邮件服务器断开连接
smtper.quit()
print('发送完成!')


if __name__ == '__main__':
main()

发送短信

发送短信也是项目中常见的功能,网站的注册码、验证码、营销信息基本上都是通过短信来发送给用户的。在下面的代码中我们使用了互亿无线短信平台(该平台为注册用户提供了50条免费短信以及常用开发语言发送短信的demo,可以登录该网站并在用户自服务页面中对短信进行配置)提供的API接口实现了发送短信的服务,当然国内的短信平台很多,读者可以根据自己的需要进行选择(通常会考虑费用预算、短信达到率、使用的难易程度等指标),如果需要在商业项目中使用短信服务建议购买短信平台提供的套餐服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import urllib.parse
import http.client
import json


def main():
host = "106.ihuyi.com"
sms_send_uri = "/webservice/sms.php?method=Submit"
# 下面的参数需要填入自己注册的账号和对应的密码
params = urllib.parse.urlencode({'account': '你自己的账号', 'password' : '你自己的密码', 'content': '您的验证码是:147258。请不要把验证码泄露给其他人。', 'mobile': '接收者的手机号', 'format':'json' })
print(params)
headers = {'Content-type': 'application/x-www-form-urlencoded', 'Accept': 'text/plain'}
conn = http.client.HTTPConnection(host, port=80, timeout=30)
conn.request('POST', sms_send_uri, params, headers)
response = conn.getresponse()
response_str = response.read()
jsonstr = response_str.decode('utf-8')
print(json.loads(jsonstr))
conn.close()


if __name__ == '__main__':
main()

网络编程入门

Posted on 2019-05-18 | In python , python100天计划 |
Words count in article: 3.9k | Reading time ≈ 14

网络编程入门

TCP/IP模型

实现网络通信的基础是网络通信协议,这些协议通常是由互联网工程任务组 (IETF)制定的。所谓“协议”就是通信计算机双方必须共同遵从的一组约定,例如怎样建立连接、怎样互相识别等,网络协议的三要素是:语法、语义和时序。构成我们今天使用的Internet的基础的是TCP/IP协议族,所谓协议族就是一系列的协议及其构成的通信模型,我们通常也把这套东西称为TCP/IP模型。与国际标准化组织发布的OSI/RM这个七层模型不同,TCP/IP是一个四层模型,也就是说,该模型将我们使用的网络从逻辑上分解为四个层次,自底向上依次是:网络接口层、网络层、传输层和应用层,如下图所示。

Read more »

动态规划

Posted on 2019-05-18 | In C++ , 算法 |
Words count in article: 0 | Reading time ≈ 1

c++ STL容器——deque list map queue set stack vector

Posted on 2019-05-18 | In C++ , 其他 |
Words count in article: 6.9k | Reading time ≈ 32

stack

1
栈的定义在头文件<stack>中,stack 模板类需要两个模板参数,一个是元素类型,一个容器类型,但只有元素类型是必要的,在不指定容器类型时,默认的容器类型为deque。

定义stack 对象的示例代码如下:

stack<int> s1;
stack<string> s2;

stack的基本操作有:对于stack<int> s

1
2
3
4
5
6
7
8
9
入     栈:      s.push(x);

出      栈:     s.pop();   //出栈只是删除栈顶元素,并不返回该元素

访问栈顶元素:   s.top();

判断栈空:    s.empty();//栈空时,返回true

栈中的元素个数:s.size();

Read more »

贪心算法

Posted on 2019-05-18 | In C++ , 算法 |
Words count in article: 3.2k | Reading time ≈ 13

贪心算法

定义:

贪心算法指的是,在对问题求解时,总是做出在当前看来是最好的选择,也就是说,不从整体最优上加以考虑,它所做出的仅是在某种意义上的局部最优解,贪心算法不是对所有问题都能的得到整体最优解,但对范围相对广泛的许多问题它能产生整体最优解或者是整体最优解的近似解。

贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。

适用于贪心算法求解的最优化问题:

  1. 最优子结构:问题的最优解包含了子问题的最优解
  2. 贪心选择性质:可通过做局部最优(贪心)选择来达到全局最优解(可行性)。

解题的一般步骤是:

  1. 建立数学模型来描述问题;
  2. 把求解的问题分成若干个子问题;
  3. 对每一子问题求解,得到子问题的局部最优解;
  4. 把子问题的局部最优解合成原来问题的一个解。
Read more »

排序算法

Posted on 2019-05-18 | In C++ , 算法 |
Words count in article: 2.5k | Reading time ≈ 10

排序算法可以分为内部排序和外部排序。

内部排序是数据记录在内存中进行排序。

而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。

常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。

1

关于时间复杂度:

  • 平方阶 (O(n2)) 排序 各类简单排序:直接插入、直接选择和冒泡排序。

  • 线性对数阶 (O(nlog2n)) 排序 快速排序、堆排序和归并排序;

  • O(n1+§)) 排序,§ 是介于 0 和 1 之间的常数。 希尔排序

  • 线性阶 (O(n)) 排序 基数排序,此外还有桶、箱排序。

关于稳定性:

  • 稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。

  • 不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。

冒泡排序 (Bubble Sort)

每次从左到右两两比较,把大的交换到后面,每次可以确保将前M个元素的最大值移动到最右边。

  • 从左开始比较相邻的两个元素x和y,如果 x > y 就交换两者
  • 执行比较和交换,直到到达数组的最后一个元素
  • 重复执行1和2,直到执行n次,也就是n个最大元素都排到了最后
1
2
3
4
5
6
7
8
9
10
11
12
void bubble_sort(vector<int> &nums)
{
for (int i = 0; i < nums.size() - 1; i++) { // times
for (int j = 0; j < nums.size() - i - 1; j++) { // position
if (nums[j] > nums[j + 1]) {
int temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
}
}
}
}

插入排序(Insertion Sort)

插入排序的原理是从左到右,把选出的一个数和前面的数进行比较,找到最适合它的位置放入,使前面部分有序。

  • 从左开始,选出当前位置的数x,和它之前的数y比较,如果x < y则交换两者
  • 对x之前的数都执行1步骤,直到前面的数字都有序
  • 选择有序部分后一个数字,插入到前面有序部分,直到没有数字可选择
1
2
3
4
5
6
7
8
9
10
11
12
void insert_sort(vector<int> &nums)
{
for (int i = 1; i < nums.size(); i++) { // position
for (int j = i; j > 0; j--) {
if (nums[j] < nums[j - 1]) {
int temp = nums[j];
nums[j] = nums[j - 1];
nums[j - 1] = temp;
}
}
}
}

选择排序(Selection Sort)

选择排序的原理是,每次都从乱序数组中找到最大(最小)值,放到当前乱序数组头部,最终使数组有序。

  • 从左开始,选择后面元素中最小值,和最左元素交换
  • 从当前已交换位置往后执行,直到最后一个元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void selection_sort(vector<int> &nums)
{
for (int i = 0; i < nums.size(); i++) { // position
int min = i;
for (int j = i + 1; j < nums.size(); j++) {
if (nums[j] < nums[min]) {
min = j;
}
}

int temp = nums[i];
nums[i] = nums[min];
nums[min] = temp;
}
}

!!!

希尔排序(Shell Sort)

希尔排序从名字上看不出来特点,因为它是以发明者命名的。它的另一个名字是“递减增量排序算法“。这个算法可以看作是插入排序的优化版,因为插入排序需要一位一位比较,然后放置到正确位置。为了提升比较的跨度,希尔排序将数组按照一定步长分成几个子数组进行排序,通过逐渐减短步长来完成最终排序。

例子

例如 [10, 80, 70, 100, 90, 30, 20] 如果我们按照一次减一半的步长来算, 这个数组第一次排序时以3为步长,子数组是:

10 80 70 90 30 20 100

这里其实按照列划分的4个子数组,排序后结果为

10 30 20 90 80 70 100

也就是 [10, 30 20 90 80 70 100]

然后再以1为步长生成子数组

10 30 20 ..

这个时候就是一纵列了,也就是说最后一定是以一个数组来排序的。

步骤

  • 计算当前步长,按步长划分子数组
  • 子数组内插入排序
  • 步长除以2后继续12两步,直到步长最后变成1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void shell_sort(vector<int> &nums)
{
for (int gap = nums.size() >> 1; gap > 0; gap >>= 1) { // times
for (int i = gap; i < nums.size(); i++) { // position
int temp = nums[i];

int j = i - gap;
for (; j >= 0 && nums[j] > temp; j -= gap) {
nums[j + gap] = nums[j];
}

nums[j + gap] = temp;
}
}
}

归并排序(Merge Sort)

归并排序是采用分治法(Divide and Conquer)的一个典型例子。这个排序的特点是把一个数组打散成小数组,然后再把小数组拼凑再排序,直到最终数组有序。

步骤

  • 把当前数组分化成n个单位为1的子数组,然后两两比较合并成单位为2的n/2个子数组
  • 继续进行这个过程,按照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
// L = 左边起始位置, R = 右边起始位置, RightEnd = 右边终点位置
void Merge(ElementType A[], ElementType TmpA[], int L, int R, int RightEnd)
{
int LeftEnd, NumElements, Tmp;
int i;

LeftEnd = R-1;
/* 有序序列的起始位置*/
Tmp = L;
NumElements = RightEnd-L+1;

while( L <= LeftEnd && R <= RightEnd)
{
if(A[L] <= A[R])
TmpA[Tmp++] = A[L++];
else
TmpA[Tmp++] = A[R++];
}

while(L <= LeftEnd)
TmpA[Tmp++] = A[L++];
while(R <= RightEnd)
TmpA[Tmp++] = A[R++];

for(i = 0; i < NumElements; i++, RightEnd--)
{
A[RightEnd] = TmpA[RightEnd]; //将有序的TmpA[]复制回A[]
}
}

void Msort(ElementType A[], ElementType TmpA[], int L, int RightEnd)
{
int Center;
if( L < RightEnd )
{
Center = (L + RightEnd) / 2;
Msort(A, TmpA, L, Center);
Msort(A, TmpA, Center+1, RightEnd);
Merge(A, TmpA, L, Center + 1, RightEnd);
}
}

循环实现:

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
//归并排序- 循环实现
//这里Merge函数在递归版本中给出
//length = 当前有序子列的长度
void Merge_pass(ElementType A[], ElementType TmpA[], int N, int length)
{
//两两归并相邻有序子列
int i, j;
for(i = 0; i <= N-2*length; i += 2*length)
Merge(A, TmpA, i, i+length, i+2*length-1);
if(i + length < N) //归并最后两个子列
Merge(A, TmpA, i, i+length, N-1);
else //最后只剩下一个子列
for(j = i; j < N; j++)
TmpA[j] = A[j];
}

void Merge_Sort(ElementType A[], int N)
{
int length;
ElementType * TmpA;

length = 1;
TmpA = malloc(N * sizeof(ElementType));
if(TmpA != NULL)
{
while(length < N)
{
Merge_pass(A, TmpA, N, length);
length *= 2;
Merge_pass(TmpA, A, N, length);
length *= 2;
}
free( TmpA);
}
else
printf("空间不足");
}

这个实现中加了一个temp,是和原数组一样大的一个空间,用来临时存放排序后的子数组的。

快速排序(Quick Sort)

快速排序也是利用分治法实现的一个排序算法。快速排序和归并排序不同,它不是一半一半的分子数组,而是选择一个基准数,把比这个数小的挪到左边,把比这个数大的移到右边。然后不断对左右两部分也执行相同步骤,直到整个数组有序。

步骤

  • 用一个基准数将数组分成两个子数组
  • 将大于基准数的移到右边,小于的移到左边
  • 递归的对子数组重复执行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
void quick_sort(vector<int> &nums, int b, int e, vector<int> &temp)
{
int m = (b + e) / 2;
if (m != b) {
int lb = b, rb = e - 1;

for (int i = b; i < e; i++) {
if (i == m)
continue;
if (nums[i] < nums[m])
temp[lb++] = nums[i];
else
temp[rb--] = nums[i];
}
temp[lb] = nums[m];

for (int i = b; i < e; i++)
nums[i] = temp[i];

quick_sort(nums, b, lb, temp);
quick_sort(nums, lb + 1, e, temp);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void quick_sort(vector<int> &nums, int b, int e)
{
if (b < e - 1) {
int lb = b, rb = e - 1;
while (lb < rb) {
while (nums[rb] >= nums[b] && lb < rb)
rb--;
while (nums[lb] <= nums[b] && lb < rb)
lb++;
swap(nums[lb], nums[rb]);
}
swap(nums[b], nums[lb]);
quick_sort(nums, b, lb);
quick_sort(nums, lb + 1, e);
}
}

堆排序(Heap Sort)

堆排序经常用于求一个数组中最大k个元素时。因为堆实际上是一个完全二叉树,所以用它可以用一维数组来表示。因为最大堆的第一位总为当前堆中最大值,所以每次将最大值移除后,调整堆即可获得下一个最大值,通过一遍一遍执行这个过程就可以得到前k大元素,或者使堆有序。

在了解算法之前,首先了解在一维数组中节点的下标:

  • i节点的父节点 parent(i) = floor((i-1)/2)
  • i节点的左子节点 left(i) = 2i + 1
  • i节点的右子节点 right(i) = 2i + 2

步骤

  • 构造最大堆(Build Max Heap):首先将当前元素放入最大堆下一个位置,然后将此元素依次和它的父节点比较,如果大于父节点就和父节点交换,直到比较到根节点。重复执行到最后一个元素。
  • 最大堆调整(Max Heapify):调整最大堆即将根节点移除后重新整理堆。整理方法为将根节点和最后一个节点交换,然后把堆看做n-1长度,将当前根节点逐步移动到其应该在的位置。
  • 堆排序(HeapSort):重复执行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
void heap_sort(vector<int> &nums)
{
int n = nums.size();
for (int i = n / 2 - 1; i >= 0; i--) { // build max heap
max_heapify(nums, i, nums.size() - 1);
}

for (int i = n - 1; i > 0; i--) { // heap sort
int temp = nums[i];
num[i] = nums[0];
num[0] = temp;
max_heapify(nums, 0, i);
}
}

void max_heapify(vector<int> &nums, int beg, int end)
{
int curr = beg;
int child = curr * 2 + 1;
while (child < end) {
if (child + 1 < end && nums[child] < nums[child + 1]) {
child++;
}
if (nums[curr] < nums[child]) {
int temp = nums[curr];
nums[curr] = nums[child];
num[child] = temp;
curr = child;
child = 2 * curr + 1;
} else {
break;
}
}
}

1

二分查找法

Posted on 2019-05-18 | In C++ , 算法 |
Words count in article: 3.4k | Reading time ≈ 13

关于二分查找法

二分查找法主要是解决在“一堆数中找出指定的数”这类问题。

而想要应用二分查找法,这“一堆数”必须有一下特征:

  • 存储在数组中
  • 有序排列
    所以如果是用链表存储的,就无法在其上应用二分查找法了。

至于是顺序递增排列还是递减排列,数组中是否存在相同的元素都不要紧。不过一般情况,我们还是希望并假设数组是递增排列,数组中的元素互不相同。


数组和链表的区别

数组:

数组是将元素在内存中连续存放,由于每个元素占用内存相同,可以通过下标迅速访问数组中任何元素。但是如果要在数组中增加一个元素,需要移动大量元素,在内存中空出一个元素的空间,然后将要增加的元素放在其中。同样的道理,如果想删除一个元素,同样需要移动大量元素去填掉被移动的元素。如果应用需要快速访问数据,很少或不插入和删除元素,就应该用数组。

链表:

链表恰好相反,链表中的元素在内存中不是顺序存储的,而是通过存在元素中的指针联系到一起。比如:上一个元素有个指针指到下一个元素,以此类推,直到最后一个元素。如果要访问链表中一个元素,需要从第一个元素开始,一直找到需要的元素位置。但是增加和删除一个元素对于链表数据结构就非常简单了,只要修改元素中的指针就可以了。如果应用需要经常插入和删除元素你就需要用链表数据结构了。

C++语言中可以用数组处理一组数据类型相同的数据,但不允许动态定义数组的大小,即在使用数组之前必须确定数组的大小。而在实际应用中,用户使用数组之前有时无法准确确定数组的大小,只能将数组定义成足够大小,这样数组中有些空间可能不被使用,从而造成内存空间的浪费。链表是一种常见的数据组织形式,它采用动态分配内存的形式实现。需要时可以用new分配内存空间,不需要时用delete将已分配的空间释放,不会造成内存空间的浪费。

  (1) 从逻辑结构角度来看
   a, 数组必须事先定义固定的长度(元素个数),不能适应数据动态地增减的情况。当数据增加时,可能超出原先定义的元素个数;当数据减少时,造成内存浪费。
   b,链表动态地进行存储分配,可以适应数据动态地增减的情况,且可以方便地插入、删除数据项。(数组中插入、删除数据项时,需要移动其它数据项)
  (2)从内存存储角度来看
   a,(静态)数组从栈中分配空间, 对于程序员方便快速,但自由度小。
   b, 链表从堆中分配空间, 自由度大但申请管理比较麻烦.

数组和链表的区别整理如下:

数组静态分配内存,链表动态分配内存;
数组在内存中连续,链表不连续;
数组元素在栈区,链表元素在堆区;
数组利用下标定位,时间复杂度为O(1),链表定位元素时间复杂度O(n);

数组插入或删除元素的时间复杂度O(n),链表的时间复杂度O(1)。

二分查找法的基本实现

二分查找法在算法家族大类中属于“分治法”,分治法基本都可以用递归来实现的,二分查找法的递归实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
int bsearch(int array[], int low, int high, int target)
{
if (low > high) return -1;

int mid = (low + high)/2;
if (array[mid]> target)
return binarysearch(array, low, mid -1, target);
if (array[mid]< target)
return binarysearch(array, mid+1, high, target);

//if (midValue == target)
return mid;
}

不过所有的递归都可以自行定义stack来解递归,所以二分查找法也可以不用递归实现,而且它的非递归实现甚至可以不用栈,因为二分的递归其实是尾递归,它不关心递归前的所有信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int bsearchWithoutRecursion(int array[], int low, int high, int target)
{
while(low <= high)
{
int mid = (low + high)/2;
if (array[mid] > target)
high = mid - 1;
else if (array[mid] < target)
low = mid + 1;
else //find the target
return mid;
}
//the array does not contain the target
return -1;
}

如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。 当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。 尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码。

只用小于比较(<)实现二分查找法

1
2
3
4
5
6
7
8
9
在前面的二分查找实现中,我们既用到了小于比较(<)也用到了大于比较(>),也可能还需要相等比较(==)。

而实际上我们只需要一个小于比较(<)就可以。因为错逻辑上讲a>b和b<a应该是有相当的逻辑值;而a==b则是等价于 !((a<b)||(b<a)),也就是说a既不小于b,也不大于b。

当然在程序的世界里, 这种关系逻辑其实并不是完全正确。另外,C++还允许对对象进行运算符的重载,因此开发人员完全可以随意设计和实现这些关系运算符的逻辑值。

不过在整型数据面前,这些关系运算符之间的逻辑关系还是成立的,而且在开发过程中,我们还是会遵循这些逻辑等价关系来重载关系运算符。

只用一个关系运算符,这样可以为二分查找法写一个template,又能减少对目标对象的要求。模板会是这样的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <typename T, typename V>
inline int BSearch(T& array, int low, int high, V& target)
{
while(!(high < low))
{
int mid = (low + high)/2;
if (target < array[mid])
high = mid - 1;
else if (array[mid] < target)
low = mid + 1;
else //find the target
return mid;
}
//the array does not contain the target
return -1;
}

我们只需要求target的类型V有重载小于运算符就可以。而对于V的集合类型T,则需要有[]运算符的重载。当然其内部实现必须是O(1)的复杂度,否则也就失去了二分查找的效率。

用二分查找法找寻边界值

之前的都是在数组中找到一个数要与目标相等,如果不存在则返回-1。我们也可以用二分查找法找寻边界值,也就是说在有序数组中找到“正好大于(小于)目标数”的那个数。

用数学的表述方式就是:

在集合中找到一个大于(小于)目标数t的数x,使得集合中的任意数要么大于(小于)等于x,要么小于(大于)等于t。

举例来说:

给予数组和目标数

1
2
3
int array = {2, 3, 5, 7, 11, 13, 17};
int target = 7;
那么上界值应该是11,因为它“刚刚好”大于7;下届值则是5,因为它“刚刚好”小于7。

用二分查找法找寻上界

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//Find the fisrt element, whose value is larger than target, in a sorted array 
int BSearchUpperBound(int array[], int low, int high, int target)
{
//Array is empty or target is larger than any every element in array
if(low > high || target >= array[high]) return -1;

int mid = (low + high) / 2;
while (high > low)
{
if (array[mid] > target)
high = mid;
else
low = mid + 1;

mid = (low + high) / 2;
}

return mid;
}

与精确查找不同之处在于,精确查找分成三类:大于,小于,等于(目标数)。而界限查找则分成了两类:大于和不大于。

如果当前找到的数大于目标数时,它可能就是我们要找的数,所以需要保留这个索引,也因此if (array[mid] > target)时 high=mid; 而没有减1。

用二分查找法找寻下界

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//Find the last element, whose value is less than target, in a sorted array 
int BSearchLowerBound(int array[], int low, int high, int target)
{
//Array is empty or target is less than any every element in array
if(high < low || target <= array[low]) return -1;

int mid = (low + high + 1) / 2; //make mid lean to large side
while (low < high)
{
if (array[mid] < target)
low = mid;
else
high = mid - 1;

mid = (low + high + 1) / 2;
}

return mid;
}

下界寻找基本与上界相同,需要注意的是在取中间索引时,使用了向上取整。若同之前一样使用向下取整,那么当low == high-1,而array[low] 又小于 target时就会形成死循环。因为low无法往上爬超过high。

这两个实现都是找严格界限,也就是要大于或者小于。如果要找松散界限,也就是找到大于等于或者小于等于的值(即包含自身),只要对代码稍作修改就好了:

去掉判断数组边界的等号:

target >= array[high]改为 target > array[high]
在与中间值的比较中加上等号:

array[mid] > target改为array[mid] >= target

用二分查找法找寻区域

之前我们使用二分查找法时,都是基于数组中的元素各不相同。假如存在重复数据,而数组依然有序,那么我们还是可以用二分查找法判别目标数是否存在。不过,返回的index就只能是随机的重复数据中的某一个。

此时,我们会希望知道有多少个目标数存在。或者说我们希望知道数组的区域。

结合前面的界限查找,我们只要找到目标数的严格上届和严格下届,那么界限之间(不包括界限)的数据就是目标数的区域了。

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
//return type: pair<int, int>
//the fisrt value indicate the begining of range,
//the second value indicate the end of range.
//If target is not find, (-1,-1) will be returned
pair<int, int> SearchRange(int A[], int n, int target)
{
pair<int, int> r(-1, -1);
if (n <= 0) return r;

int lower = BSearchLowerBound(A, 0, n-1, target);
lower = lower + 1; //move to next element

if(A[lower] == target)
r.first = lower;
else //target is not in the array
return r;

int upper = BSearchUpperBound(A, 0, n-1, target);
upper = upper < 0? (n-1):(upper - 1); //move to previous element

//since in previous search we had check whether the target is
//in the array or not, we do not need to check it here again
r.second = upper;

return r;
}

它的时间复杂度是两次二分查找所用时间的和,也就是O(log n) + O(log n),最后还是O(log n)。

在轮转后的有序数组上应用二分查找法

之前我们说过二分法是要应用在有序的数组上,如果是无序的,那么比较和二分就没有意义了。

不过还有一种特殊的数组上也同样可以应用,那就是“轮转后的有序数组(Rotated Sorted Array)”。它是有序数组,取期中某一个数为轴,将其之前的所有数都轮转到数组的末尾所得。比如{7, 11, 13, 17, 2, 3, 5}就是一个轮转后的有序数组。非严格意义上讲,有序数组也属于轮转后的有序数组——我们取首元素作为轴进行轮转。

下边就是二分查找法在轮转后的有序数组上的实现(假设数组中不存在相同的元素)

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
int SearchInRotatedSortedArray(int array[], int low, int high, int target) 
{
while(low <= high)
{
int mid = (low + high) / 2;
if (target < array[mid])
if (array[mid] < array[high])//the higher part is sorted
high = mid - 1; //the target would only be in lower part
else //the lower part is sorted
if(target < array[low])//the target is less than all elements in low part
low = mid + 1;
else
high = mid - 1;

else if(array[mid] < target)
if (array[low] < array[mid])// the lower part is sorted
low = mid + 1; //the target would only be in higher part
else //the higher part is sorted
if (array[high] < target)//the target is larger than all elements in higher part
high = mid - 1;
else
low = mid + 1;
else //if(array[mid] == target)
return mid;
}

return -1;
}

对比普通的二分查找法,为了确定目标数会落在二分后的那个部分,我们需要更多的判定条件。但是我们还是实现了O(log n)的目标。

二分查找法的缺陷

二分查找法的O(log n)让它成为十分高效的算法。不过它的缺陷却也是那么明显的。就在它的限定之上:

必须有序,我们很难保证我们的数组都是有序的。当然可以在构建数组的时候进行排序,可是又落到了第二个瓶颈上:它必须是数组。

数组读取效率是O(1),可是它的插入和删除某个元素的效率却是O(n)。因而导致构建有序数组变成低效的事情。

解决这些缺陷问题更好的方法应该是使用二叉查找树了,最好自然是自平衡二叉查找树了,自能高效的(O(n log n))构建有序元素集合,又能如同二分查找法一样快速(O(log n))的搜寻目标数。

https://www.cnblogs.com/ider/archive/2012/04/01/binary_search.html

1…293031…38
zzm99

zzm99

372 posts
40 categories
3 tags
GitHub
0%
© 2020 zzm99 | Site words total count: 409.1k
Powered by Hexo
|
Theme — NexT.Gemini v5.1.4