Quantcast
Channel: CSDN博客推荐文章
Viewing all 35570 articles
Browse latest View live

LeetCode笔记:539. Minimum Time Difference

$
0
0

问题:

Given a list of 24-hour clock time points in “Hour:Minutes” format, find the minimum minutes difference between any two time points in the list.

Example 1:

Input: [“23:59”,”00:00”]

Output: 1

Note:

  1. The number of time points in the given list is at least 2 and won’t exceed 20000.

  2. The input time is legal and ranges from 00:00 to 23:59.

大意:

给出一个 “Hour:Minutes” 形式的24小时制时间点的List,寻找List中任意两个时间点的最小分钟时间差。

例1:

输入:[“23:59”,”00:00”]

输出:1

注意:

  1. 给出的List中包含的时间点至少有两个,不超过20000。

  2. 输入的时间是合法的,而且范围在 00:00 到 23:59。

思路:

题目会给出一系列24小时制的时间,我们要找到最小的两个时间的时间差,这个差值是以分钟数表示的,为了计算方便,我们写一个函数来将所有给出的24小时制时间全部改成分钟表示,比如 1:30 用全分钟数来表示就90分钟,这样我们计算时间差就很方便,要排序也很方便。

全部转换成分钟数后,我们放在一个int型数组里,对数组排序,这样我们就可以按照拍完序后的顺序去两两比较时间点之间的时间差,看哪个时间差最小,记录下来,要注意的一点是最后一个时间要用24小时的分钟数减去他然后加上第一个时间点的时间差,得到最后一个时间点和第一个时间点的时间差。

题目说了至少会有两个时间点,所以给的List为空的情况不用考虑。

代码(Java):

public class Solution {
    public int findMinDifference(List<String> timePoints) {
        int[] minuteArr = new int[timePoints.size()];
        for (int i = 0; i < minuteArr.length; i++) {
            minuteArr[i] = transToMinute(timePoints.get(i));
        }
        Arrays.sort(minuteArr);

        int res = 24*60 - minuteArr[minuteArr.length-1] + minuteArr[0];
        for (int i = 0; i < minuteArr.length-1; i++) {
            if (minuteArr[i+1] - minuteArr[i] < res) res = minuteArr[i+1] - minuteArr[i];
        }
        return res;
    }

    public int transToMinute(String time) {
        String[] arr = time.split(":");
        int a = Integer.valueOf(arr[0]).intValue() * 60;
        int b = Integer.valueOf(arr[1]).intValue();
        return a + b;
    }
}

合集:https://github.com/Cloudox/LeetCode-Record
版权所有:http://blog.csdn.net/cloudox_

作者:Cloudox_ 发表于2017/3/19 14:57:25 原文链接
阅读:139 评论:0 查看评论

【零基础入门学习Python笔记016】中文编码

$
0
0


Python 文件中如果未指定编码,在执行过程会出现报错:

比如:

#!/usr/bin/python
print "你好,世界";

报错:

File "test.py", line 2
SyntaxError: Non-ASCII character '\xe4' in file test.py on line 2, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details

Python中默认的编码格式是 ASCII 格式,在没修改编码格式时无法正确打印汉字,所以在读取中文时会报错,注意这些中文也包括平常的中文注释。

解决方法为只要在文件开头加入 # -*- coding: UTF-8 -*- 或者 #coding=utf-8 就行了。

#!/usr/bin/python
# -*- coding: UTF-8 -*-
 
print "你好,世界";

所以如果大家在学习过程中,代码中包含中文,就需要在头部指定编码。

注意:Python3.X 源码文件默认使用utf-8编码,所以可以正常解析中文,无需指定 UTF-8 编码。

注意:如果你使用编辑器,同时需要设置好编辑器的编码,如 Pycharm 设置步骤:

  • 进入 file > Settings,在输入框搜索 encoding
  • 找到 Editor > File encodings,将 IDE EncodingProject Encoding 设置为utf-8。



作者:SMF0504 发表于2017/3/19 15:17:05 原文链接
阅读:126 评论:0 查看评论

XML基础

$
0
0

随着Internet的飞速发展,HTML因扩展困难、交互性差和语义模糊等缺点在日益增长的网络设计需求面前呈现出弱势。标准、简洁、结构严谨、可扩展性高的XML应运而生。在推出之初,XML提供通用数据交换、改变Web发布、改变分布式计算的功能。如今,XML不仅对软件开发的各个方面都产生了巨大影响,而且在各行各业都得到了充分应用。

XMLExtensible  Markup  Language的缩写,是一种类似于HTML的标记语言。XML文档的主要作用是传输数据。例如,Web应用中客户端从服务器请求到XML文档或服务器接收到客户端上传的XML文档,此时应该如何获取其中的数据? XML文档中除数据本身外,还有许多元素、属性、实体或备注等,应用程序应该如何判断数据的类型以及数据之间的关系?这些问题都属于XML文档的解析功能。本章将讲解如何使用XML DOM解析XML文档。首先,了解W3C组织对于DOM的定义及结构。其次,使用DOM提供的接口解析XML文档.包括加载XML文档到内存、获取XML文档数据、创建、修改DOM节点等。

 

1.1 . XML简介

1.1.1 XML发展历史及作

1. XML的发展历史

XML是指可扩展标记语言(Extensible  Markup  Language),类似于HTML。早在1969年,IBM公司开发了一种文档描述语言GML,用于解决不同系统文档格式的问题。该语言在1986年演变为国际标准(ISO8879),被称为SGMLSGML是许多大型组织(如飞机、汽车公司和军队)的文档标准,它是与语言无关、结构化可扩展的语言,受到许多公司的欢迎,被用于创建、处理和发布大量的文本信息。

1989年,CERN欧洲粒子物理研究中心的研究人员开发了基于SGML的超文本版本,称为HTMLHTML继承了SGML的许多重要优点,如结构化、独立性和可描述性;但同时也存在较多缺陷,如只能使用固定、有限的标记,而且只侧重于显示内容等。

随着数据的增多,HTML的缺点演变成无法忽略的问题。W3C提供了HTML的扩展来解决这些问题,最后形成了新的SGML的子集——XML

XML的出现是为了解决HTML中的弊病,它保留了许多SGML标准的优点,更易于操作以及在WWW环境下实现。1998年,XML成为W3C的标准。

XMLSGMLHTML之间的发展关系如图1.1.1所示。

1.1.1 XML的发展示意图

XMLW3C的推荐标准,其设计宗旨是传输数据而非显示数据。XML标签没有被预定义,需要自定义标签。XML具有自我描述性。

XMLHTML的主要差异体现在以下3个方面:

(1) XML不是HTML的替代。

(2) XMLHTML为了不同的目的而设计使用。

(3) XML用于传输和存储数据,旨在传输信息,关注的焦点是数据的内容;HTML用于显示数据,旨在显示信息,关注的焦点是数据的外观。

 

2. XML的作用

被誉为“万维网标准之王”的Jeffrey Zeldman曾说:“当XML(扩展标记语言)于19982月被引入软件工业界时,它给整个行业带来了一场风暴。有史以来第一次,这个世界拥有了一种用来结构化文档和数据的通用且适应性强的格式,它不仅仅可以用于Web,而且可以被用于任何地方”。

XML的主要作用可以概括为以下6个:

1)使数据从HTML分离。

如果需要在HTML文档中显示动态数据,则每次数据改变时将花费大量的时间来编辑HTML。通过XML,数据能够存储在独立的XML文件中,从而专注地使用HTML进行布局和显示,且修改底层数据时无须再次对HTML进行任何改变。通过几行JavaScript即可读取一个外部XML文件,然后更新HTML中的数据内容。

2)简化数据共享。

XML数据以纯文本格式进行存储,提供了一种独立软件和硬件的数据存储方法,使创建不同应用程序共享的数据变得更加容易。

3)简化数据传输。

通过XML可以在不兼容的系统之间轻松交换数据。对于开发人员而言,在因特网上不兼容的系统之间交换数据特别耗费时间。可以通过各种不兼容的应用程序来读取数据,使用XML交换数据可以降低这种复杂性。

4)简化平台的变更。

升级到新系统(硬件或软件平台)相对费时的情况下,不兼容的数据在转换大量的数据时经常丢失。XML数据以文本格式存储,这使XML可以在不丢失数据的前提下更易于扩展或升级到新的操作系统、应用程序或浏览器。

5)延伸了数据使用。

XML独立于硬性、软件以及应用程序之外,使数据更可用、有用。不同的应用程序都能够在HTML页面以及XML数据源中访问。通过XML的数据不仅可以供各种阅读设备(手持计算机、语音设备、新闻阅读器等)使用,还可以供盲人等残障人士使用。

6)创建新的Internet语言。通过XML创建的新的Internet语言如下:

1XHTML:最新的HTML版本。

2WSDL:用于描述可用的Web Service

3WAPWML:用于手持设备的标记语言。

4RSS:用于RSS Feed的语言。

5RDFOWL:用于描述资源和本体。

6SMIL:用于描述针对Web的多媒体。

1.1.2 XML的语法和结构

1. XML 的结构

 

note.xml:

<?xml  version="1.0" encoding="gb2312"?>

<note>

<to>张明</to>

<from>小红</from>

<heading>Message</heading>

<body>今晚8点公司全体员工一起聚餐!</body>

</note>

对示例1.1的说明如下:

(1) 第一行是XML声明其定义XML的版本(1.0)和所使用的编码(字符集)。

(2) 第二行是文档的根元素,根据标签名称理解本文档的作用(便签)。

(3) 第三行到第六行描述了根元素的4个子元素——tofromheadingbody

(4) 最后一行定义根元素的结尾:</note>

由上述说明可知,该XML文档包含一张小红写给张明的便签,可以从中领略到XML出色的自我描述能力。

XML文档形成树状结构。表述书籍XML文档见示例1.2,分析该文档可以了解文档的树结构。

 

books.xml:

<?xml  version="1.0" encoding="ISO-8859-1"?>

<bookstore>

<book category="COOKING">

<title  lang="en">Everyday  Italian</title>

<author>Giada De Laurentiis</author>

<year>2005</year>

<price>30.00</price>

</book>

<book category="CHILDREN">

<title lang="en">Harry  Potter</title>

<author>J K.Rowling</author>

<year>2005</year>

<price>29.99</price>

</book>

<book category="Web">

<title lang="en">Learning  XML</title>

          <author>Erik T. Ray</author>

    <year>2003</year>

     <price>39.95</price>

     </book>

</bookstore>

文档形成的树结构如图1.1.2所示。

 

 

 

 

 

 

 

 

 

 

 

1.1.2 XML文档树结构

示例1.2中根元素是 <bookstore>,文档中所有的<book>元素都被包含在<bookstore>中。<book>元素包含4个子元素。分别为<title><author><year><price><book>元素具有属性category<title>元素具有属性lang

2XML的语法规则

1XML文档应该遵循以下规则:

1)  XML文档必须有根元素。

2)  XML文档必须有关闭标签。

3)  XML标签对大小写敏感。

4)  XML元素必须被正确地嵌套。

5)  XML属性必须加引号。与HTML相同,XML的属性由“名称/值”对组成。

6)  XML中的注释使用“<!——注释的内容——>”标注。

7)  XML中的空格将被保留。

HTML可以将多个连续的空格字符裁减为一个。而在XML中,文档中的空格不会被裁减,例如:

Hello    world

HTML中输出:

Hello world

XML中输出:

Hello    world

2XML文档的元素。

XML元素是从开始标签(且包括)直到结束标签(且包括)的部分。元素可以包含其他元素、文本或者二者的混合物。元素可以拥有属性。

在示例1.2中,<bookstore><book>都拥有元素内容,因为它们包含其他元素;<author>只有文本内容,因为它仅包含文本;<book>元素拥有属性category

XML元素是可扩展的。示例1.3对示例1.l中的根元素<note>扩展<date>元素,表示发便签的日期。

 

exnotexml

<?xml version="10" encoding="gb2312"?>

<note>

<date>2011214</date><!——扩展的新元素——>

    <to>张明</to>

    <from>小红</from>

   <heading>Message</heading>

     <body>今晚8点公司全体员工一起聚餐!</body>

</note>

 

XML元素名称必须遵循以下命名规则:

Ø 可以包含字母、数字以及其他字符。

Ø 不能以数字或标点符号开始。

Ø 不能以字符xml”(或者XMLxml)开始。

Ø 不能包含空格。

3XML文档的属性。

HTML类似,XML元素可以在开始标签中包含属性,属性(Attribute)提供关于元素的额外信息。属性值必须加双引号,XML元素通常可以在开始标签中包含属性。绝大部分的属性都可以使用元素替代。

 

Personsxml

<?xml version="1.0encoding="gb2312"?>

    <persons>

     <person  sex="male">

     <name>Jack</name>

     </person>

     <person>

     <sex>male</sex>

     <name>Jack</name>

     </person>

</persons>

XML中并未规定使用属性以及需要使用元素的场合,一般可以使用属性的位置都可以使用子元素替换。

 

<!--1.使用date属性-->

<?xml version="1.0" encoding="UTF-8"?>

<note date="2011214">

<to>Ben</to>

    <from>Jack</from>

    <heading>Message</heading>

    <body>今晚8点公司全体员工一起聚餐!</body>

</note>

<!--2.使用date子元素-->

<?xml version="1.0"  encoding="UTF-8"?>

<note>

    <date>2011214</date>

    <to>Ben</to>

    <from>Jack</from>

    <heading>Message</heading>

    <body>今晚8点公司全体员工一起聚餐!</body>

</note>

<!-- 3.扩展date元素 -->

<?xml version="1.0" encoding="UTF-8"?>

<note>

    <date>

     <day>14</day>

     <month>2</month>

     <year>2011</year>

    </date>

    <to>Ben</to>

    <from>Jack</from>

    <heading>Message</heading>

    <body>今晚8点公司全体员工一起聚餐!</body>

</note>

属性存在以下缺点:

1) 无法包含多个值(子元素可以)。

2) 无法描述树结构(子元素可以)。

3) 不易扩展、不利于后续复用。

4) 难以阅读和维护。

所以,实际应用中建议尽量使用元素描述数据,而使用属性提供与数据无关的信息。

综上所述,元数据(有关数据的数据)应当存储为属性,而数据本身应当存储为元素。例如,

实际应用中向元素分配ID引用,这些lD可用于标识XML元素。其用法与HTMLlD属性相同,作用仅仅是标识符,而非组成数据的一部分,此时应该使用属性。

 

studentsxml

<?xml version="1.0" encoding="UTF-8"?>

<students>

     <student ID="s01">

     <name>Tom</name>

     <sex>male</sex>

     <age>20</age>

     <class>G224</class>

     </student>

     <student ID="s02">

     <name>Anna</name>

     <sex>female</sex>

     <age>19</age>

     <class>G330</class>

     </student>

</students>

4XML文档的实体。

XML中,部分字符具有特殊的意义。例如,将字符“<”放在XML元素中会发生错误,因为解析器会将它作为新元素的开始。以下代码行会产生XML错误:

<message>if  salary  <  1000  then</messaqe>

为了避免此类错误,可以使用一个实体引用代替<”字符,代码如下:

<message>if salary  &1t; 1000  then</message>

XML中包含5个预定义的实体引用,见表1-1-1

实体

数据

说明

<

<

小于

>

>

大于

&

&

'

'

单引号

"

"

双引号

实体引用类似于编程语言中的引用型变量,用户也可以自定义实体。

 

1.2   DOM及其模型

1.2.1  DOM简介

1.什么是DOM

DOM即文档对象模型,是Document Obiect Model的缩写,定义了访问XMLXHTML等文档的标准。DOMW3C标准,W3CDOM定义为“一个使程序和脚本有能力动态地访问和更新文档的内容、结构以及样式的平台和语言中立的接口”。DOM分为核心DOMHTML DOMXML DOM。其中,核心DOM是用于任何结构化文档的标准模型。

2.什么是HTML DOM

HTML DOM定义了所有HTML文档元素的对象和属性以及访问它们的方法(接口),是用于HTML文档的标准模型。

3.什么是XML DOM

HTML DOM相似,XML DOM定义了所有XML元素的对象和属性以及访问它们的方法(接口),是用于获取、更改、添加或删除XML元素的标准。XML DOM包括以下4个含义:

1)用于XML的标准对象模型。

2)用于XML的标准编程接口。

3)独立于平台和语言。

4W3C的标准。

XML DOM的作用如图1.1.3所示。

 

 

                           

 

 

 

 

                        

 

 

 

                      

1.1.3 XML DOM的作用

 

4DOM级别

DOM级别指W3C组织的DOM规范。W3C组织为DOM规范定义了3个级别:

1DOM级别1:专注于HTMLXML文档模型,含有文档导航和处理功能。

2DOM级别2:在DOM级别1的基础上添加了样式表对象模型,并定义了操作文档样式信息的功能。同时,DOM级别2定义了一个事件模型,并提供对XML命名空间的支持。

1DOM Level 2 Core:规定了访问和更改文档内容及结构的API,该API同时包含用于XML的接口。

2DOM Level 2 HTML:规定了操作HTML文档结构和内容的API

3DOM Level 2 Views:规定了对文档视图进行访问和更改的API。视图是与原文档相关联的表现形式或某种备用的表现形式。

4DOM Level 2 Style:规定了动态访问及更改内容样式表的API

5DOM Level 2 Events:规定了访问文档事件的API

6DOM Level 2 Traversal-Range:规定了动态遍历和识别文档中内容范围的API

3DOM级别3:定义了内容模型(DTDSchema)和文档验证。同时,规定了文档加载和保存、文档查看、文档格式化和关键事件。DOM Level 3建立在DOM Level 2核心之上。

1DOM Level 3 Core:规定了访问和更改文档内容、结构及样式的API

2DOM Level 3 Events:通过添加新的接口和事件集,DOM Level 3 Events APILevel 2 Event API的功能进行了扩展.

3DOM Level 3 Content Model:规定了用于内容加载和保存、内容模型(DTDSchema)和文档验证支持的API

4DOM Level 3 Views:规定了对文档视图进行访问和更改的API

1.2.2 节点和节点树

依据DOM的规定.XML文档中每个单元(元素、属性、实体、备注等)都是节点。.例如:

1)整个文档是一个文档节点。

2)每个XML标签是一个元素节点。

3)包含在XML元素中的文本是文本节点。

4)每个XML属性是一个属性节点。

5)注释属于注释节点。

XML DOMXML文档视为树结构.这种树结构被称为节点树。程序通过节点树访问所有节点、修改或删除其内容以及创建新元素。节点树展示了节点的集合以及它们之间的关系。节点树从根节点开始,在树的最低层级向文本节点长出“枝条”。

XML节点树中的节点如图1.1.4所示。

1.1.4 XML文档节点树

 

booksxml

<?xml version="1.0" encoding="UTF-8"?>

<bookstore>

<book category="children">

<title lang="en">Harry Potter</title>

<author>J K. Rowling</author>

<year>2005</year>

<price>29.99</price>

<publish pubdate="2011-01-09"/>

</book>

<book category="cooking">

<title lang="en">Everyday Italian</title>

<author>Giada De Laurentiis</author>

<year>2005</year>

<price>30.00</price>

</book>

   <book category="web">

<title lang="en">Learning XML</title>

<author>Erik T. Ray</author>

<year>2003</year>

<price>39.95</price>

</book>

   <book category="web">

<title lang="en">XQuery Kick Start</title>

<author>James McGovern</author>

<author>Per Bothner</author>

<author>Kurt Cagle</author>

<author>James Linn</author>

<author>Vaidyanathan Nagarajan</author>

<year>2003</year>

<price>49.99</price>

</book>

</bookstore>

将示例1.7中的booksxml使用节点树表示.如图1.1.5所示。

 

 

 

 

 

 

 

 

 

 

1.1.5XML DOM节点树

booksxml文件的说明如下:

1)根节点是<bookstore>,文档中所有的其他节点都包含在<bookstore>中。

2)根节点<bookstore>包含4<book>节点。

3)第一个<book>节点包含4个节点:<title><author><year><price>。其中,每个节点包含一个文本节点(分别为Harry PotterJ KRowling200529.99)。

4)元素节点的文本存储在文本节点中。<year>2005</year>中,元素节点<year>拥有一个值为“2005”的文本节点,“2005”不是<year>元素的值。

5) 属性节点与子元素属于同一级别的节点。例如,<book>元素的属性“category”与<book>

的子元素<title>同级。

1.父级、子级和同级节点

节点树中的节点彼此之间存在等级关系,可以使用父级、子级和同级节点描述这种关系。父节点拥有子节点,位于相同层级上的子节点称为同级节点。

1)在节点树中,顶端的节点为根节点。

2)根节点之外的每个节点都有一个父节点。

3)节点可以有任何数量的子节点。

4)叶节点是没有子节点的节点。

5)同级节点是拥有相同父节点的节点。

节点之间的关系如图1.1.6所示。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

                                       

1.1.6节点树中节点之间的关系

2.第一个子节点和最后一个子节点

 

<book category="web">

<title lang="en">Learning XML</title>

<author>Erik T. Ray</author>

<year>2003</year>

<price>39.95</price>

</book>

在示例1.8中,<title>元素是<book>元素的第一个子节点,而<price>元素是<book>元素的最

后一个子节点。

 

XML数据按照节点树的形式进行构造,可以在不了解节点树的确切结构以及其中包含的数据类型的情况下,对其进行遍历。

1.3  DOM编程接口

1.3.1 XML DOM对象

XML DOM对象中封装了常用的操作XML文档的属性和方法。常用的XML DOM对象如下:

1XML DOM Attr对象:表示Element对象的属性。

2XML DOM CDATASection对象:表示文档中的CDATA段。

3XML DOM Comment对象:表示文档中注释节点的内容。

4XML DOM Document对象:表示整个XML文档。

5XML DOM DOMParser对象:该对象通过解析XML标记创建一个文档。

6XML DOM Element对象:表示XML文档中的元素。

7XML DOM NamedNodeMap对象:表示一个无序的节点列表。

8XML DOM Node对象:表示文档树中的一个节点。

9XML DOM NodeList对象:表示一个有序的节点列表。

10XML DOM Text对象:表示元素或属性的文本内容。

11XML DOM XMLHttpRequest对象:提供对HTTP协议的访问,包括发出POSTHEAD以及普通GET请求的能力。XMLHttpRequest可以同步或异步地返回Web服务器的响应,并且通过文本或者一个DOM文档的形式返回内容。此对象并不限于和XML文档一起使用,可以接收任何形式的文本文档。

1.3.2 加载XML文档

浏览器都内建了用于读取和操作XMLXML解析器。解析器将XML读人内存,并转换为可以被JavaScript访问的XML DOM对象。

XML数据可以通过XML文档保存在磁盘介质上,或者通过XML字符串在内存中创建。XMLDocument对象将XML文档和XML字符串加载到内存,然后通过JavaScript实现DOM解析。如果XML文档需要在服务端解析,还可以使用C#Java等编程语言通过DOM API进行解析。

使用JavaScript实现DOM时,不同浏览器的加载方式有所不同。可以通过异常处理来编写通用浏览器的加载方法。

1.使用load()方法加载XML文档

 

//IE浏览器

xmlDoc = new ActiveXObject("Microsoft.XMLDOM");

xmlDoc.async = false;

xmlDoc.load("books.xml");

 

//FirefoxMozilla等浏览器

xmlDoc = document.implementation.createDocument("", "", null);

xmlDoc.async = false;

xmlDoc.load("books.xml");

在示例1.9和示例1.10中,第一行代码用于在浏览器中创建空的XML Document对象;第二行关闭异步加载,可以确保在文档完整加载之前,解析器不会继续执行脚本;第三行通知解析器加载名为“books.xml”的文档。

示例1.11通过异常处理模块编写跨浏览器的XML文档加载代码。

 

<script type="text/javascript">

try {

xmlDoc = new ActiveXObject("Microsoft.XMLDOM");

} catch (e) {

try {

xmlDoc = document.implementation.createDocument("", "", null);

} catch (e) {

alert(e.message)

}

}

try {

xmlDoc.async = false;

xmlDoc.load("books.xml");

document.write("XML文档加载完毕,节点数已经创建,可以进行解析了.")

} catch (e) {

alert(e.message);

}

</script>

示例1.11在浏览器中的运行效果如图1.1.7所示。

 

1.1.7 XML文档加载

Java语言中,DOM是使用与平台和语言无关的方式表示XML文档的官方W3C标准,是以层次结构组织的节点或信息片断的集合。该层次结构允许开发人员在树中寻找特定信息,分析该结构通常需要加载整个文档和构造层次结构,然后才能执行其他工作。该结构基于信息层次,因而DOM被认为是基于树或基于对象的。

 

//使用DOM 加载XML文件

public static void loadXmlDom() throws Exception {

//查找工程相对目录下的XML文件

File f = new java.io.File("WebRoot/books.xml");

DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

DocumentBuilder builder = factory.newDocumentBuilder();

document = builder.parse(f);

System.out.println("XML文档加载完毕,节点数已经创建,可以进行解析了.");

}

示例1.12的运行效果如图1.1.8所示。

 

1.1.8 XML Document对象加载XML文档

2.使用loadXML ()方法加载XML字符串

IE浏览器中使用XML Document对象的loadXML() 方法加载XML字符串,在Firefox等浏览器中使用DOM解析器对象DOMParserparseFromString() 方法加载XML字符串。

 

<script type="text/javascript">

//定义XML字符串

var text = "<bookstore>";

text = text+"<book>";

text = text + "<title>Learning jQuery</title>";

text = text + "<author>Jonathan Chaffer</author>";

text = text + "<year>2008</year>";

text = text + "</book>";

text = text + "</bookstore>";

try {

xmlDoc = new ActiveXObject("Microsoft.XMLDOM");

} catch (e) {

try {

xmlDoc = document.implementation.createDocument("", "", null);

} catch (e) {

alert(e.message)

}

}

try {

xmlDoc.async = false;

xmlDoc.loadXML(text);

document.write("XML文档加载完毕,节点数已经创建,可以进行解析了.")

} catch (e) {

alert(e.message);

}

</script>

Java语言中,可以使用DOM4J类库的SAXReader类的read()方法加载XML字符串。

 

//使用DOM4J类库加载XML字符串

public static void loadXmlStringDom() throws Exception {

//定义XML字符串

String text = "<bookstore>";

text = text+"<book>";

text = text + "<title>Learning jQuery</title>";

text = text + "<author>Jonathan Chaffer</author>";

text = text + "<year>2008</year>";

text = text + "</book>";

text = text + "</bookstore>";

Document doc =DocumentHelper.parseText(text); // 将字符串转为XML;

Element rootElt = doc.getRootElement(); // 获取根节点

Iterator iters = rootElt.elementIterator("book");

while (iters.hasNext()) {

Element itemEle = (org.dom4j.Element) iters.next();

         String title = itemEle.elementTextTrim("title");

         String year = itemEle.elementTextTrim("year");

         System.out.println("title:" + title);

         System.out.println("year:" + year);

    }

}

 

 

public void testRead() throws Exception{

SAXReader reader = new SAXReader();

Document document = reader.read(new FileInputStream("src/output.xml"));

Element root = document.getRootElement();

for (Iterator iter = root.elementIterator(); iter.hasNext();) {

Element element = (Element) iter.next();

System.out.println(element.getText());

System.out.println(element.attributeValue("name"));

System.out.println(element.attributeValue("blog"));

}

}

public void testWrite() throws Exception{

Document document = DocumentHelper.createDocument();

Element root = document.addElement("root");

for(int i=0;i<10;i++){

Element element1 = root.addElement("user")

.addAttribute("name","Alex"+i)

.addAttribute("id", "id"+i)

.addText("我是信息");

}

XMLWriter writer = new XMLWriter(new FileOutputStream("src/output.xml"));

writer.write(document);

writer.close();

}

1.3.3 节点操作

节点操作通过XML DOM对象的属性和方法实现。对象多达几十个,且每个对象都有各自的属性和方法,常用对象的属性和方法见表1-1-2、表1-1-3

1-1-2 XML DOM对象常用属性

属性   

说明

nodeName

获取节点名称

nodeValue

获取节点的值

parentNode

获取节点的父节点

childNodes

获取节点的所有子节点集合

attributes

获取当前节点所有的属性节点

documentElement

获取文档的根节点

1-1-3  XML DOM对象常用方法

方法

说明

getElementsByTagName(name)

获取带有指定标签名(name)的所有元素

CreateElement(name)

创建指定标签名的元素节点

appendChild(node)

向调用节点末尾插入子节点node

removeChild(node)

从调用节点中删除子节点node

 

 

//使用属性和方法从booksxml中的<title>元素中获取文本的JavaScript代码

var txt=xmlDoc.getElementsByTagName("title")[0].childNodes[0].nodeValue;

对示例1.15的说明如下:

1xmlDoc:由解析器创建的XML Document对象,创建方式见示例1.11

2getElementsByTagName("title")[0]:获取第一个<title>元素。

3childNodes[0]:获取<title>元素的第一个子节点(文本节点)。

4nodeValue:获取节点的值(文本自身)。

 

//使用属性和方法从booksxml中的<title>元素中获取文本的Java代码

String title=docgetElementsByTagName("title")

 .item(0).getFirstChiid().getNodeValue();

示例1.16中,使用getNodeValue方法获取节点的值。

另外,DOM对象的许多属性和方法并没有列出。DOM只提供接口和API,不同语言有不同的实现方式,但一般区别较小。例如,JavaScriptJava语言的DOM实现只有部分属性或方法的名称存在差异。

1.访问节点

访问节点包括遍历节点、定位节点、获取节点的详细信息等操作,这些操作都通过XML DOM

的属性和方法实现。通常,访问节点需要使用对象XML Node ListXML Node。前者表示一个节点列表(集合),后者表示一个节点。每个节点都具有nodeNamenodeValuenodeType属性,分别用于获得节点名称、节点值和节点的类型。

1XML Node List

XML Node List代表一个节点集合,具有length属性,通过该属性可以获取节点的个数,并对节点进行遍历。在Java语言中,使用NodeList类的对象表示一个节点集合。

2XML Node

XML Node表示一个节点。具有nodeNamenodeTypenodeValue属性。元素节点还可以通过attributes属性返回属性节点的列表。在Java语言中,使用Node类的对象表示XML Node

1nodeName。其特点是:①nodeName是只读的;②元素节点的nodeName与标签名相同;③属性节点的nodeName是属性的名称。

2nodeTypenodeType属性规定节点的类型,是只读的。nodeType常用的值包括:①“1”表示节点是元素节点:②“2”表示节点是属性节点;③“3”表示节点是文本节点;④“8”表示节点是注释节点:⑤“9”表示节点是文档节点。

3nodeValue。特点:①元素节点的nodeValue是不可用的;②文本节点的nodeValue是文本自身;③属性节点的nodeValue是属性的值。

可以通过以下3种方法访问节点:

1)使用getElementsByTagName()方法。

2)循环(遍历)节点树。

3)通过节点的关系在节点树中导航。

DOM中,节点的关系被定义为节点的属性,定位节点的属性见表1-1-4

1-1-4  DOM中表示节点关系的常用属性

属性

功能

parentNode

获取父节点

childNodes

获取子节点集合

firstChild

获取第一个子节点

lastChild

获取最后一个子节点

nextSibling

获取同级别中后一个节点

previousSibling

获取同级别中前一个节点

 

<script type="text/javascript">

    try {

xmlDoc = new ActiveXObject("Microsoft.XMLDOM");

} catch (e) {

try {

xmlDoc = document.implementation.createDocument("", "", null);

} catch (e) {

alert(e.message)

}

}

try {

xmlDoc.async = false;

xmlDoc.load("books.xml");

document.write("XML文档加载完毕,节点数已经创建,可以进行解析了.")

} catch (e) {

alert(e.message);

}

var nlist=xmlDoc.getElementsByTagName("book")[0].childNodes;

document.write("<br>book节点长度: " + nlist.length );

document.write("<br>");

for (var i=0;i<nlist.length;i++){

var node = nlist[i];

if (node.nodeType==1){  //元素节点

     document.write("元素节点:" + node.nodeName);

}

var att=node.attributes;//获取该元素的全部属性节点

for(var j=0;j<att.length;j++){

document.writeln("<br/>元素"+node.nodeName+

 "有属性"+att[i].nodeName+"="+att[i].nodeValue);

}

document.write("<br/>");

}

</script>

示例1.17在浏览器中的执行效果如图1.1.9所示。

 

1.1.9使用JavaScript实现DOM访问节点

使用Java语言编程,实现与示例1.17相同的功能,代码见示例1.18

 

//访问节点

public static void loadXmlDomNode() throws Exception {

loadXmlDom();//调用加载XML方法

String title = document.getElementsByTagName("title").item(0)

   .getFirstChild().getNodeValue();

System.out.println("title: " + title);

//获得book节点集合

NodeList bookList = document.getElementsByTagName("book");

System.out.println("book节点的长度: " +bookList.getLength());

int size = bookList.item(0).getChildNodes().getLength();

NodeList firstBook = bookList.item(0).getChildNodes();

for (int i = 0; i < size; i++) {

Node bookNode = firstBook.item(i);

if (bookNode.getNodeType() == 1) {

System.out.println("元素节点: " + bookNode.getNodeName() + " = "

    + bookNode.getTextContent());

}

try {

NamedNodeMap att = bookNode.getAttributes();

for (int j = 0; j < att.getLength(); j++) {

System.out.println("元素: " + bookNode.getNodeName()

+ " 有属性 "

+ att.item(j).getNodeName() + "="

+ att.item(j).getNodeValue());

}

} catch (Exception e) { e.getMessage(); }

}

}

示例1.18的执行结果如图1.1.10所示。

 

1.1.10使用Java语言实现DOM访问节点

另外,使用Java语言可以遍历元素节点的值,代码如下:

//使用JAVA遍历元素节点值

public static void queryXmlDom() throws Exception {

loadXmlDom();//调用加载XML方法

//获得book节点集合

NodeList bookList = document.getElementsByTagName("book");

for (int i = 0; i < bookList.getLength(); i++) {

Element student = (Element) bookList.item(i);

NodeList bookTempList = student.getChildNodes();

System.out.println("title: " + bookTempList.item(1).getTextContent());

System.out.println("auth: " + bookTempList.item(3).getTextContent());

System.out.println("year: " + bookTempList.item(5).getTextContent());

System.out.println("price: " + bookTempList.item(7).getTextContent());

System.out.println("----------------------");

}

}

 遍历XML节点的效果如图1.1.11所示。

 

1.1.11使用Java语言实现DOM遍历节点

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

本章总结

Ø XMLW3C的推荐标准,其设计宗旨是传输数据而非显示数据。XML标签没有被预定义,需要自定义标签。XML具有自我描述性。

Ø XML语法规则。

n XML文档必须有根元素。

n XML文档必须有关闭标签。

n XML标签对大小写敏感。

n XML元素必须被正确地嵌套。

n XML属性必须加引号。与HTML相同,XML的属性由“名称/值”对组成。

n XML中的注释使用“<!——注释的内容——>”标注。

n XML中的空格将被保留。

Ø DOM即文档对象模型,是Document Obiect Model的缩写,定义了访问XMLXHTML等文档的标准。

Ø DOMW3C标准,W3CDOM定义为“一个使程序和脚本有能力动态地访问和更新文档的内容、结构以及样式的平台和语言中立的接口”。

Ø XML数据可以通过XML文档保存在磁盘介质上,或者通过XML字符串在内存中创建。XMLDocument对象将XML文档和XML字符串加载到内存,然后通过JavaScript实现DOM解析。

Ø 如果XML文档需要在服务端解析,还可以使用C#Java等编程语言通过DOM API进行解析。

 

 

 

 

 

 

 

 

 

任务实训部分 

1编写XML文件

训练技能点

Ø 编写XML文档

Ø 熟悉XML文档的格式要求

需求说明

编写XML文档,存储表1-2-1中的数据。

1-2-1 数据内容

分类编号

产品编号

产品名称

1

1

奥迪

1

2

宝马

1

24

花冠

1

34

雷克萨斯

1

35

林肯

1

38

雪佛兰

1

39

标致

1

43

法拉利

1

67

凌志

1

70

帕萨特

1

75

高尔夫

1

76

别克

 

实现步骤

(1) 新建文本文件,重命名为products.xml,使用记事本打开该文件。

(2) 在文件中输入XML声明:
<?xml version="1.0" encoding="UTF-8"?>

(3) XML声明后输入以下代码,并保存文件products.xml

<?xml version="1.0" encoding="gb2312" ?>

<Categories  分类编号="1">

<Products>

<Product 产品编号="1" 产品名称="奥迪"/>

<Product 产品编号="2" 产品名称="宝马"/>

<Product 产品编号="24" 产品名称="花冠"/>

<Product 产品编号="34" 产品名称="雷克萨斯"/>

<Product 产品编号="35" 产品名称="林肯"/>

<Product 产品编号="38" 产品名称="雪佛兰"/>

<Product 产品编号="39" 产品名称="标致"/>

<Product 产品编号="43" 产品名称="法拉利"/>

<Product 产品编号="67" 产品名称="凌志"/>

<Product 产品编号="70" 产品名称="帕萨特"/>

<Product 产品编号="75" 产品名称="高尔夫"/>

<Product 产品编号="76" 产品名称="别克"/>

</Products>

</Categories>

4)使用浏览器打开文件 products.xml ,显示效果如图

 

1.2.1 products.xml在浏览器中的显示

 

2:使用JavaScript加载并解析XML文档

训练技能点

Ø 使用load()方法加载XML文档并解析。

需求说明

根据提供的XML文档,使用JavaScript语言实现加载该XML文档到XML解析器。遍历所有节点并在浏览器显示。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>

<students>

  <student no="m01">

    <name>Tom</name>

    <class>G3-67</class>

    <sex></sex>

    <birth>

      <year>1990</year>    

      <month>2</month>

      <day>21</day>

    </birth>

    <skill>XHTML</skill>

    <skill>.NET</skill>

    <skill>JAVA</skill>

    <skill>Oracle</skill>

  </student>

 <student no="m02">

    <name>Katherine</name>

    <class>G3-65</class>

    <sex></sex>

    <birth>

      <year>1989</year>    

      <month>10</month>

      <day>3</day>

    </birth>

    <skill>XHTML</skill>

    <skill>.NET</skill>

    <skill>JAVA</skill>

  </student>

</students>

实现步骤

(1) 区分IE浏览器和FireFox浏览器中创建文档对象的方式。

(2) IE使用xmlDoc=new ActiveXObject("Microsoft.XMLDOM"); 创建空XML DOM对象。

(3) Firefox使用xmlDoc=document.implementation.createDocument("","",null); 创建空XML DOM对象。

(4) 使用浏览器打开loadXml.html,显示效果如图2.2.1所示。

 

1.2.2加载解析的XML文件

参考代码

<script type="text/javascript">

try{

xmlDoc=new ActiveXObject("Microsoft.XMLDOM");

}catch(e){

try{

xmlDoc=document.implementation.createDocument("","",null);

}catch(e){

alert(e.message);

}

}

try{

xmlDoc.async=false;

xmlDoc.load("students.xml");

var output="";

var node=xmlDoc.getElementsByTagName("student");

for(var i=0;i<node.length;i++){

output+="<"+node[i].nodeName;

output+=" "+node[i].attributes[0].nodeName+"=""+node[i].getAttribute("no")+""";

output+="><br/>";

for(var j=0;j<node[i].childNodes.length;j++){

output+="    ";

output+="<"+node[i].childNodes[j].nodeName+">";

output+=node[i].childNodes[j].childNodes[0].nodeValue;

output+="</"+node[i].childNodes[j].nodeName+">";

output+="<br/>";

}

output+="</"+node[i].nodeName+">"+"<br/>";

}

document.writeln(output);

}catch(e){

alert(e.message);

}

</script>

3:使用JavaScript操作XML文档

需求说明

(1) 创建并添加一个节点。

(2) 添加节点<student>,节点内容如下:

<student no="m04">

<name>Mary</name>

<class>G3-70</class>

<sex>女</sex>

<birth>

<year>1991</year>

<month>6</month>

<day>22</day>

</birth>

<skill>JAVA</skill>

</student>

实现步骤

(1) 使用createElement()方法创建元素节点。

(2) 使用createTextNode()方法创建文本节点,使用nodeValue属性为文本节点赋值,使用appendChild()方法添加节点。

参考代码

添加student节点的代码片段:

<script type="text/javascript">

try{

xmlDoc=new ActiveXObject("Microsoft.XMLDOM");

}catch(e){

try{

xmlDoc=document.implementation.createDocument("","",null);

}catch(e){

alert(e.message);

}

}

try{

xmlDoc.async=false;

xmlDoc.load("students.xml");

var student=xmlDoc.createElement("student");

var att=xmlDoc.createAttribute("no");

att.nodeValue="m04";

student.setAttributeNode(att);

var name=xmlDoc.createElement("name");

name.appendChild(xmlDoc.createTextNode("Mary"));

student.appendChild(name);

var clas=xmlDoc.createElement("class");

clas.appendChild(xmlDoc.createTextNode("G3-70"));

student.appendChild(clas);

var sex=xmlDoc.createElement("sex");

sex.appendChild(xmlDoc.createTextNode(""));

student.appendChild(sex);

var birth=xmlDoc.createElement("birth");

var year=xmlDoc.createElement("year");

year.appendChild(xmlDoc.createTextNode("1991"));

birth.appendChild(year);

var month=xmlDoc.createElement("month");

month.appendChild(xmlDoc.createTextNode("6"));

birth.appendChild(month);

var day=xmlDoc.createElement("day");

day.appendChild(xmlDoc.createTextNode("22"));

birth.appendChild(day);

student.appendChild(birth);

var skill1=xmlDoc.createElement("skill");

skill1.appendChild(xmlDoc.createTextNode("JAVA"));

student.appendChild(skill1);

xmlDoc.documentElement.appendChild(student);

var output="";

var node=xmlDoc.getElementsByTagName("student");

for(var i=0;i<node.length;i++){

output+="<"+node[i].nodeName;

output+=" "+node[i].attributes[0].nodeName+"=""+node[i].getAttribute("no")+""";

output+="><br/>";

for(var j=0;j<node[i].childNodes.length;j++){

output+="    ";

output+="<"+node[i].childNodes[j].nodeName+">";

output+=node[i].childNodes[j].childNodes[0].nodeValue;

output+="</"+node[i].childNodes[j].nodeName+">";

output+="<br/>";

}

output+="</"+node[i].nodeName+">"+"<br/>";

}

document.writeln(output);

}catch(e){

alert(e.message);

}

</script>

 

4:使用JAVA解析XML文档

训练技能点

Ø 使用JAVA实现解析XML文档。

需求说明

(1) XML文档内容如下:

<?xml version="1.0" encoding="UTF-8"?>

<?xml-stylesheet type="text/xsl" href="cdsort.xsl"?>

<catalog>

<cd>

<title>Made in India</title>

<artist>Jack</artist>

<country>India</country>

<company>Bollywood</company>

<price>10</price>

<year>2011</year>

</cd>

<cd>

<title>摩羯座</title>

<artist>Alun</artist>

<country>China</country>

<company>Bollywood</company>

<price>5</price>

<year>2010</year>

</cd>

</catalog>

(2) 要求得到所有CDtitleartistpriceyear这些节点值。

实现步骤

(1) 加载XML文档。

(2) 使用DOM解析XML文档。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

巩固练习

一、选择题

1.以下针对XML文档的定义和规范的有(  )

  A.DTD

  B.SGML

  CSchema

  D.complexType

2.以下关于XML的语法规则的说法中,错误的有(  )

  AXML文档必须有且只有一个根元素

  BXML文档中的标签(元素)区分大小写

  CXML文档中的元素和属性都必须加引号

  DXML文档中一个属性可以包含多个值

3.以下关于DOM级别的说法中,正确的有(  )

ADOM级别是W3C组织的DOM规范

BDOM级别l专注于文档模型

CDOM级别2专注于文档模型

DDOM级别2规定了DTDSchema

4.以下关于XML节点树的描述,错误的是(  )

  A.同级节点拥有相同的父节点

  B.除根节点外,所有节点都有一个父节点

  C.有且须只有一个根节点

  D所有节点都必须有子节点

5.以下对XML DOM对象的属性描述中,正确的有(  )

  A.通过nodeName获取文本节点的名称,其名称全部都是“#text

  BnodeType=l表示当前节点是属性节点

  C.元素节点的nodeValue的值是undefined

  DnextSibling属性获取相同级别、名称的下一个节点

二、简答题

1.简述XML的作用。

2.简述节点树中节点之间的关系。

作者:zhangchen124 发表于2017/3/19 15:32:10 原文链接
阅读:142 评论:0 查看评论

PAT (Advanced Level) Practise 1001. A+B Format (20)

$
0
0

1001. A+B Format (20)

时间限制
400 ms
内存限制
65536 kB
代码长度限制
16000 B
判题程序
Standard
作者
CHEN, Yue

Calculate a + b and output the sum in standard format -- that is, the digits must be separated into groups of three by commas (unless there are less than four digits).

Input

Each input file contains one test case. Each case contains a pair of integers a and b where -1000000 <= a, b <= 1000000. The numbers are separated by a space.

Output

For each test case, you should output the sum of a and b in one line. The sum must be written in the standard format.

Sample Input
-1000000 9
Sample Output
-999,991
原题链接:https://www.patest.cn/contests/pat-a-practise/1001

题意:计算两个数的和,结果加千分符(,)分隔.

原以为要用字符串处理,后来一想,用递归就可以解决,万能的递归.

AC代码:

#include <iostream>
#include <cstdio>
using namespace std;
void fun(int x)
{
	if(x/1000>0)
	{
		fun(x/1000);
		printf(",%03d",x%1000);//不足三位添零补 
	}
	else
	{
		cout<<x%1000;
	}
}
int main()
{
	int a,b;
	while(cin>>a>>b)
	{
		int c=a+b;
		if(c<0)
		{
			cout<<"-";
			c=-c;
		}
		fun(c);
		cout<<endl;
	}
	return 0;
}




作者:hurmishine 发表于2017/3/19 15:41:06 原文链接
阅读:123 评论:0 查看评论

iOS中OC给Category添加属性

$
0
0

引:

很多人知道可以用Category给已有的类添加一些新方法,但是不同于swift中的extension,Objective-C中的Category(类别)是不支持直接添加属性的,那如果就是需要添加新的属性怎么办呢?有一个办法,使用Runtime中的关联对象技术就可以实现。关于Runtime的说明可以查看这篇博客:传送门:OC中Runtime浅析

Category(类别):

对于不熟悉Category的人,这里也先说明一下Category是什么以及怎么创建Category。

由于一些特殊的需要,我们可能要给现有的类添加一些新的方法,这个需求用继承也可以做到,但是会显得比较重,这时候就可以用Category来达到目的,创建一个已有类的Category,可以给这个类添加你需要的方法,在使用的时候,只需要import你创建的Category,在使用的时候还是使用原来的类,但是你会惊奇的发现他支持你自己在Category中添加的方法。

我们看到的一些名为类似“UINavigationController+Cloudox.h”的文件就是类别了。

那么怎么创建类别呢?

在工程中按住command+N来添加新文件,选择Objectiv-C File:

在出来的界面中的File Type选择Category,就是创建类别文件了,File填写我们要加在类别尾巴上的名字,Class选择你要添加类别的已有类,这里我们为UINavigationController添加类别:

类别文件这样就创建成功了。

添加属性

类别可以为已有的类添加方法,但是却不能直接添加属性,因为即使你添加了@property,它既不会生成实例变量,也不会生成setter、getter方法,即使你添加了也无法使用。

所以我们首先需要自己去添加setter、getter方法,这个好办,直接在.m文件里加就可以了,但是要真正添加可以使用的属性,还需要利用Runtime来关联对象,关于关联对象的技术可以看传送门:OC中Runtime浅析,这里只讲怎么用来添加属性,我们在setter方法里关联一个对象,在getter方法里获取对应key关联的对象,就可以啦,代码如下,很简单:

//UINavigationController+Cloudox.h文件
#import <UIKit/UIKit.h>

@interface UINavigationController (Cloudox)

@property (copy, nonatomic) NSString *cloudox;

@end

//UINavigationController+Cloudox.m文件
#import "UINavigationController+Cloudox.h"
#import <objc/runtime.h>

@implementation UINavigationController (Cloudox)

//定义常量 必须是C语言字符串
static char *CloudoxKey = "CloudoxKey";

-(void)setCloudox:(NSString *)cloudox{
    /*
     objc_AssociationPolicy参数使用的策略:
     OBJC_ASSOCIATION_ASSIGN;            //assign策略
     OBJC_ASSOCIATION_COPY_NONATOMIC;    //copy策略
     OBJC_ASSOCIATION_RETAIN_NONATOMIC;  // retain策略

     OBJC_ASSOCIATION_RETAIN;
     OBJC_ASSOCIATION_COPY;
     */
    /*
     关联方法:
     objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);

     参数:
     * id object 给哪个对象的属性赋值
     const void *key 属性对应的key
     id value  设置属性值为value
     objc_AssociationPolicy policy  使用的策略,是一个枚举值,和copy,retain,assign是一样的,手机开发一般都选择NONATOMIC
     */

    objc_setAssociatedObject(self, CloudoxKey, cloudox, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

-(NSString *)cloudox{
    return objc_getAssociatedObject(self, CloudoxKey);
}


@end

注意要使用Runtime需要 import runtime 的框架,如代码所示。

现在我们试试效果,我们按照平常的方式用UINavigationController包装一个控制器作为根视图:

// AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    ViewController *vc = [[ViewController alloc] init];
    UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc];
    self.window.rootViewController = nav;
    return YES;
}

这个视图控制器中我们需要导入我们的类别文件,然后我们就会发现我们能够通过self.navigationController获取到我们新添加的名为“cloudox”的属性了!我们可以给这个NSString类型的属性赋值,然后获取它进行显示:

#import "UINavigationController+Cloudox.h"

- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"Demo";
    self.view.backgroundColor = [UIColor lightGrayColor];

    // 给UINavigationController新加的属性赋值
    self.navigationController.cloudox = @"Hey,this is category's new property!";

    UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(([UIScreen mainScreen].bounds.size.width-300)/2, 100, 300, 50)];
    label.textColor = [UIColor whiteColor];
    label.textAlignment = NSTextAlignmentCenter;
    label.text = self.navigationController.cloudox;
    [self.view addSubview:label];
}

效果如下:

正常获取到并且显示,非常棒。

以上就是给Category添加属性的方法啦,不难,只要了解Runtime中的关联对象技术就可以轻松达到了。


示例工程:https://github.com/Cloudox/CategoryAddPropertyDemo
版权所有:http://blog.csdn.net/cloudox_

作者:Cloudox_ 发表于2017/3/19 16:41:47 原文链接
阅读:371 评论:0 查看评论

Leetcode 211 Add and Search Word - Data structure design

$
0
0

Design a data structure that supports the following two operations:

void addWord(word)
bool search(word)

search(word) can search a literal word or a regular expression string containing only letters a-z or .. A . means it can represent any one letter.

For example:

addWord("bad")
addWord("dad")
addWord("mad")
search("pad") -> false
search("bad") -> true
search(".ad") -> true
search("b..") -> true

Note:
You may assume that all words are consist of lowercase letters a-z.

设计数据结构存储单词,并满足用简单的正则表达式查询。

在字典树这题的基础上修改http://blog.csdn.net/accepthjp/article/details/63254529

为了满足正则匹配符点的模糊查询,将search方法改为递归形式。注意判断要查询的下一个节点是否存在

class node  
{  
    public:  
    int flag;  
    node* next[26];  
    node(int x = 0)  
    {  
        flag = x;  
        memset(next, 0, sizeof(next));  
    }  
};  
class WordDictionary {
public:
    node* root;
    /** Initialize your data structure here. */
    WordDictionary() {
        root = new node();
    }
    /** Adds a word into the data structure. */
    void addWord(string word) {
        node* p = root;  
        for(int i = 0; i < word.size(); i++)  
        {  
            if(!p->next[word[i] - 'a']) p->next[word[i] - 'a'] = new node();  
            p = p->next[word[i] - 'a'];  
        }  
        p->flag = 1; 
    }
    /** Returns if the word is in the data structure. A word could contain the dot character '.' to represent any one letter. */
    bool search(string word) {
        return dfs(word, root);
    }
    bool dfs(string word, node* now)
    {
        if(word.size() == 0) 
        {
            if(now->flag) return true;
            return false;
        }
        if(word[0]!='.')
        {
            if(!now->next[word[0] - 'a']) return false;
            return dfs(word.substr(1), now->next[word[0]-'a']);
        }
        else
            for(int i = 0; i < 26; i++) if(now->next[i] && dfs(word.substr(1), now->next[i])) return true;
        return false;
    }
};
/**
 * Your WordDictionary object will be instantiated and called as such:
 * WordDictionary obj = new WordDictionary();
 * obj.addWord(word);
 * bool param_2 = obj.search(word);
 */


作者:u012614906 发表于2017/3/19 17:13:37 原文链接
阅读:132 评论:0 查看评论

第10章 内部类

$
0
0

内部类

将一个类的定义放在另一个类的定义内部,这就是内部类。

链接到外部类:

创建自己的内部类时,内部类的对象同时拥有指向外围对象(这些对象封装或生成了内部类)的一个链接。能访问那个封装对象的所有成员。

内部类拥有对其外围类所有元素的访问权限。 

.this.new:

生成对外部类对象的引用,可以使用外部类的名字后面紧跟.this

看下面的例子:

package innerclasses;
public class DoThis {
    void f(){
        System.out.println("DoThis.f()");
    }
    public class Inner{
        public DoThis outer(){
            return DoThis.this;
        }
    }    
    public Inner inner(){
        return new Inner();
    }    
    public  static void main(String[] args){
        DoThis dt = new DoThis();
        DoThis.Inner dti = dt.inner();
        dti.outer().f();
    }
}

有时你可能想要告知某些其他对象,去创建其某个内部类的对象。要实现此目的,你必须在new表达式中提供对其他外部类对象的引用,这时需要使用.new语法。

看下面的例子:

package innerclasses;
public class DoNew {
    public class Inner{
         Inner(){
            System.out.println("DoNew.Inner");
        }
    }
    public static void main(String[] args){
        DoNew dn = new DoNew();
        DoNew.Inner dni = dn.new Inner();
    }
}

关于内部类的特性,TIJ书中的内容讲的很混乱,这里整理另一本教材的知识,更便于理解:

Java编程设计之网络编程基础教程》清华大学出版社

内部类的定义:

  • 在另一个类或一个接口中声明一个类
  • 在另一个接口或一个类中声明一个接口
  • 在一个方法中声明一个类(只有默认权限)
  • 类和接口的声明可以嵌套任意深度

内部类的特性:

  • 可以使用外部类的静态成员变量和实例成员变量,也可以使用它所在方法的局部变量。
  • 可以定义为abstract
  • 可以声明为privateprotected
  • 若被声明为static,就变成了顶层类(也叫嵌套类),不在能使用局部变量
  • 若想在内部类中声明任何static成员,则该内部类必须声明为static

匿名内部类:

匿名内部类没有名称,没法引用,使用new创建。只能扩展一个类或是一个接口。

看下面的例子:

return new Contents() {
	private int i = 11;
	public int value() { return i; }
}; 

其实相当于:

class MyContents extends Contents {
	private int i = 11;
	public int value() { return i; }
}
return new MyContents();

使用内部类的意义:

内部类允许继承多个非接口类型(类或抽象类)。

内部类标识符:

先是封装类的名字,再跟随一个$,再跟随内部类的名字。

例如,由 InheritInner.java 创建的.class 文件包括:
InheritInner.class
WithInner$Inner.class
WithInner.class 

闭包:

一个可调用对象,它记录了一些信息,这些信息来自创建它的作用域。

通过这个定义,内部类是面向对象的闭包。

作者:qq_18738333 发表于2017/3/19 17:35:21 原文链接
阅读:105 评论:0 查看评论

【数据结构】图

$
0
0

本篇博文旨在介绍数据结构中的图;介绍了图以及图的有关概念;介绍了图的两种实现方式,并用代码进行了实现;介绍并实现了图的广度优先遍历和深度优先遍历;


图是数据结构中的一种非线性结构,由顶点以及顶点相连的边构成

图包括有向图和无向图

无向图中,当两个顶点存在连接关系时,不区分该连接是A到B点,还是B到A点


有向图中,两个顶点存在连接关系时,要区分是A可以到B,还是B可以到A


图的基本概念

完全图

图中,每个顶点都与其他顶点有连线,有N*(N-1)/2条边

权重

若图中的边具有数值信息,那么该数值就为该边的权重

临接顶点

一条边的两个顶点互为邻接顶点

一个顶点具有相邻边的个数

路径

图中的一个顶点A,通过其他顶点(x1,x2,x3....)到达一个顶点B后,这中间经过的顶点就为A顶点B顶点之间的路径

连通图和强连通图

在无向图中,当一个顶点A可以通过其他顶点到达顶点B时,A,B两个顶点就互为连通

如果一个图中的所有顶点都是连通的,那么该无向图称为连通图

在有向图中,若每一对顶点都存在路径,则称此图为强连通图

生成树

一个无向连通图的生成树是它的极小连通子图,若图中有N-1个顶点,那么该图的生成树有N-1个顶点

图的两种存储方式

邻接矩阵

将所有顶点的信息组成顶点表, 利用二维矩阵来表示图中的连通关系


邻接矩阵实现图

//方法1:邻接矩阵

template<typename V,typename W>
class GraphMatrix
{
public:
	GraphMatrix(V* vertexs, size_t n, const W& invalid = W(), bool IsDirected = false)
		:_vertexs(vertexs, vertexs + n)
		, _isDirected(IsDirected)
	{
		_matrix = new W*[n];
		for (size_t i = 0; i < n; ++i)
		{
			_matrix[i] = new W[n];
			for (size_t j = 0; j < n; ++j)
			{
				_matrix[i][j] = invalid;
			}
		}
	}

	~GraphMatrix()
	{}

	int GetIndex(const V& v)
	{
		for (size_t i = 0; i < _vertexs.size(); ++i)
		{
			if (_vertexs[i] == v)
				return i;
		}
		assert(false);
		return -1;
	}

	void AddEdge(const V& v1, const V& v2, const W& w)
	{
		size_t src = GetIndex(v1);
		size_t des = GetIndex(v2);
		_matrix[src][des] = w;

		if (_isDirected == false)
			_matrix[des][src] = w;
	}
protected:
	vector<V> _vertexs;//定点的集合
	W** _matrix;//邻接矩阵边的集合
	bool _isDirected;//是否是有向图
};

void TestGraphMatrix()
{
	string city[] = { "北京", "上海", "广州", "杭州", "西安" };
	GraphMatrix<string, double> gpm(city, sizeof(city) / sizeof(city[0]));
	gpm.AddEdge("北京", "上海", 300.3);
	gpm.AddEdge("北京", "广州", 850.5);
	gpm.AddEdge("北京", "杭州", 299);
	gpm.AddEdge("北京", "西安", 475);
}

邻接表

使用数组存储顶点的信息,用链表来对链接关系进行存储


邻接表实现图

//方法2:用邻接表实现图
template<typename V,typename W>
class GraphLink
{
	typedef Edge<W> Node;
public:
	GraphLink()
	{}

	GraphLink(V* vertexs, size_t n, bool IsDirected = false)
	{
		_vertexs.resize(n);
		for (size_t i = 0; i < n; ++i)
		{
			_vertexs[i] = vertexs[i];
		}
		_linkTables.resize(n, NULL);
	}

	~GraphLink()
	{}

	int GetIndex(const V& v)
	{
		for (size_t i = 0; i < _vertexs.size(); ++i)
		{
			if (v == _vertexs[i])
				return i;
		}
		assert(false); 
		return -1;
	}

	void _AddEdge(const size_t& src, const size_t& des, const W&w)
	{
		//进行头插
		Node* newNode = new Node(src,des, w);
		newNode->_next = _linkTables[src];
		_linkTables[src] = newNode;
	}
	void AddEdge(const V& v1, const V& v2, const W& w)
	{
		size_t src = GetIndex(v1);
		size_t des = GetIndex(v2);
		_AddEdge(src, des, w);

		if (_isDirected == false)
			_AddEdge(des, src, w);
	}
protected:
	vector<V> _vertexs;//顶点的集合,用来存储顶点的值
	vector<Node*> _linkTables;//边的集合
	bool _isDirected;//是否是有向图
};

邻接矩阵和邻接表的对比

1、当一个图需要经常查找两点之间的权值关系时,利用邻接矩阵进行查询,直接定位,效率高,时间复杂度为O(1)

而邻接表需要对链接表进行一一遍历,如果该点链接的顶点比较多,查找起来效率很低,时间复杂度为O(N)

2、当我们从存储空间上比较时,邻接表又是优于邻接矩阵的

邻接表只需要存储链接的顶点,而邻接矩阵却保存了所有顶点的关系(包括不连通的两个顶点的权值)

结论:一般情况下,稀疏图用邻接表,稠密图用邻接矩阵

图的深度优先和广度优先遍历

深度优先遍历

这里我们可以根据二叉树的前序遍历理解,当遍历一条路径时,一直访问直到其没有路径时返回,再遍历其他的路径

这里我们用递归实现

代码实现

注意:此代码需要些到邻接表实现的图里

	//用哈希来判断一个定点是否被访问过
	//时间复杂度为O(1)
	void DFS_ByVector(const V& src)
	{
		size_t isrc = GetIndex(src);

		//定义vector,用于判断是否访问过
		vector<bool> v;
		v.resize(_vertexs.size(),false);
		v[isrc] = true;
		_DFS_ByVector(isrc,v);
		cout << endl;
	}
	//判断定点是否访问过的时间复杂度为O(1)
	void _DFS_ByVector(const size_t src, vector<bool>&v)
	{
		Node* cur = _linkTables[src];
		cout << GetV(src) << " ";
		while (cur)
		{
			size_t dst = cur->_des;
			if (v[dst] == false)
			{
				v[dst] = true;
				_DFS_ByVector(dst, v);
			}
			cur = cur->_next;
		}
	}

广度优先遍历

这里我们也是利用二叉树来理解,这次用的是层序遍历,每一次遍历与该节点相邻的所有节点,遍历完成后,再遍历与相邻节点直接相邻的节点。

代码实现

注意:此代需要写到邻接表实现的图里

	void BFS_ByVector(const V&src)
	{
		//利用队列来存储
		queue<size_t> q;
		size_t isrc = GetIndex(src);
		q.push(isrc);
		
		//利用vector进行判断定点是否被访问过
		vector<bool> v;
		v.resize(_vertexs.size(), false);
		v[isrc] = true;

		while (!q.empty())
		{
			size_t tmp = q.front();
			cout << GetV(tmp) << " ";
			q.pop();
			Node* cur = _linkTables[tmp];
			while (cur)
			{
				size_t dst = cur->_des;
				//如果没有访问该节点,就进行访问,并标记
				if (v[dst] == false)
				{
					v[dst] = true;
					q.push(dst);
				}
				cur = cur->_next;
			}
		}
		cout << endl;
	}

最小生成树

在连通图中,有N个顶点,那么用N-1条边来所有顶点串起来,并且没有环

并且,这N-1条边都是连通图本身的边

贪心算法

在解决问题的时候,总是做出在当前情况下的最优解。

注意:当前最优,也就是局部最优。并不能保证整体最优

用贪心算法实现最小生成树的两种算法

Kruskal算法

每次选出权重最小的边,若该边的加入不会导致出现环,那么加入该边

下面的图截自数据结构书籍


Prim算法

从一个顶点开始,选取该点邻接的权重最小的边,如果该边不会造成环的出现,那么加入该边


克鲁斯卡尔求最小生成树的算法步骤

1、利用优先级队列将边按照权值的大小排列(由小到大)

2、依次访问优先级队列的以一个元素,如果最小生成树加上该边,没有构成环,则加入这条边

3、将优先级队列的首元素出队

注意点:

并查集(前篇博文有讲,不理解的童鞋可以点击链接进行查看http://blog.csdn.net/qq_31828515/article/details/60590370)来判断一条边的两个点是否在一个集合中

如果已经在一个集合中,那么加入该边就会造成环的出现,就不加入该边

如果不在,就加入该边

下面给出克鲁斯卡尔算法的最小生成树的代码实现

注:此代码需要放入上面邻接表实现图的代码中

//求最小生成树
	bool Kruskal(GraphLink<V, W>& mintree)
	{
		mintree._vertexs = _vertexs;
		mintree._linkTables.resize(_vertexs.size(), NULL);
		mintree._isDirected = _isDirected;

		struct Compare
		{
			bool operator()(Node* l, Node* r)
			{
				return l->_weight < r->_weight;
			}
		};

		//定义优先级队列,把所有边进行存储
		priority_queue<Node*,vector<Node*>,Compare> pq;
		for (size_t i = 0; i < _vertexs.size(); ++i)
		{
			Node* cur = _linkTables[i];
			while (cur)
			{
				//只存储定顶点数比目标定点小的
				if (cur->_src < cur->_des)
					pq.push(cur);

				cur = cur->_next;
			}
		}

		//定义并查集,判断加入的两个定点是否带环
		UnionFindSet ufs(_vertexs.size());

		while (!pq.empty())
		{
			Node* top = pq.top();
			pq.pop();
			size_t isrc = top->_src;
			size_t ides = top->_des;

			//如果没有造成环,添加此边
			if (ufs.IsInSet(isrc, ides) == false)
			{
				//添加该线
				mintree._AddEdge(isrc,ides,top->_weight);
				mintree._AddEdge(ides,isrc,top->_weight);
				ufs.Union(isrc, ides);

				//所有元素在一个集合里,返回true
				if (ufs.SetSize() == 1)
					return true;
			}

		}
		return false;
	}

图的完整代码

请看GitHub链接 https://github.com/haohaosong/DataStruct/blob/master/Graph.h

作者:qq_31828515 发表于2017/3/19 17:52:34 原文链接
阅读:111 评论:0 查看评论

深度学习之图像修复

$
0
0

图像修复问题就是还原图像中缺失的部分。基于图像中已有信息,去还原图像中的缺失部分。

从直观上看,这个问题能否解决是看情况的,还原的关键在于剩余信息的使用,剩余信息中如果存在有缺失部分信息的patch,那么剩下的问题就是从剩余信息中判断缺失部分与哪一部分相似。而这,就是现在比较流行的PatchMatch的基本思想。

CNN出现以来,有若干比较重要的进展:

  • 被证明有能力在CNN的高层捕捉到图像的抽象信息。
  • Perceptual Loss的出现证明了一个训练好的CNN网络的feature map可以很好的作为图像生成中的损失函数的辅助工具。
  • GAN可以利用监督学习来强化生成网络的效果。其效果的原因虽然还不具可解释性,但是可以理解为可以以一种不直接的方式使生成网络学习到规律。

基于上述三个进展,参考文献[1]提出了一种基于CNN的图像复原方法。

CNN网络结构

该算法需要使用两个网络,一个是内容生成网络,另一个是纹理生成网络。内容生成网络直接用于生成图像,推断缺失部分可能的内容。纹理生成网络用于增强内容网络的产出的纹理,具体则为将生成的补全图像和原始无缺失图像输入进纹理生成网络,在某一层feature_map上计算损失,记为Loss NN。

内容生成网络需要使用自己的数据进行训练,而纹理生成网络则使用已经训练好的VGG Net。这样,生成图像可以分为如下几个步骤:

定义缺失了某个部分的图像为x0

  • x0输入进内容生成网络得到生成图片x
  • x作为最后生成图像的初始值
  • 保持纹理生成网络的参数不变,使用Loss NN对x进行梯度下降,得到最后的结果。

关于内容生成网络的训练和Loss NN的定义,下面会一一解释

内容生成网络

生成网络结构如上,其损失函数使用了L2损失和对抗损失的组合。所谓的对抗损失是来源于对抗神经网络.

在该生成网络中,为了是训练稳定,做了两个改变:

  • 将所有的ReLU/leaky-ReLU都替换为ELU层
  • 使用fully-connected layer替代chnnel-wise的全连接网络。

纹理生成网络

纹理生成网络的Loss NN如下:

它分为三个部分,即Pixel-wise的欧式距离,基于已训练好纹理网络的feature layer的perceptual loss,和用于平滑的TV Loss。

α和β都是5e-6

Pixel-wise的欧氏距离如下:

TV Loss如下:

Perceptual Loss的计算比较复杂,这里利用了PatchMatch的信息,即为缺失部分找到最近似的Patch,为了达到这一点,将缺失部分分为很多个固定大小的patch作为query,也将已有的部分分为同样固定大小的patch,生成dataset PATCHES,在匹配query和PATCHES中最近patch的时候,需要在纹理生成网络中的某个layer的激活值上计算距离而不是计算像素距离。

但是,寻找最近邻Patch这个操作似乎是不可计算导数的,如何破解这一点呢?同MRF+CNN类似,在这里,先将PATCHES中的各个patch的的feature_map抽取出来,将其组合成为一个新的卷积层,然后得到query的feature map后输入到这个卷积层中,最相似的patch将获得最大的激活值,所以将其再输入到一个max-pooling层中,得到这个最大值。这样,就可以反向传播了。

高清图像上的应用

本算法直接应用到高清图像上时效果并不好,所以,为了更好的初始化,使用了Stack迭代算法。即先将高清图像down-scale到若干级别[1,2,3,…,S],其中S级别为原图本身,然后在级别1上使用图像均值初始化缺失部分,得到修复后的结果,再用这个结果,初始化下一级别的输入。以此类推。

效果

上图从上往下一次为,有缺失的原图,PatchMatch算法,Context Decoder算法(GAN+L2)和本算法。

内容生成网络的作用

起到了内容限制的作用,上图比较了有内容生成网络和没有内容生成网络的区别,有的可以在内容上更加符合原图。

应用

图像的语义编辑,从左到右依次为原图,扣掉某部分的原图,PatchMatch结果,和本算法结果。

可知,该方法虽然不可以复原真实的图像,但却可以补全成一张完整的图像。这样,当拍照中有不想干的物体或人进入到摄像头中时,依然可以将照片修复成一张完整的照片。

总结

CNN的大发展,图像越来越能够变得语义化了。有了以上的图像复原的基础,尽可以进行发挥自己的想象,譬如:在图像上加一个东西,但是光照和颜色等缺明显不搭,可以用纹理网络进行修复。

该方法的缺点也是很明显:

  • 性能和内存问题
  • 只用了图片内的patch,而没有用到整个数据集中的数据。

参考文献

[1]. Yang C, Lu X, Lin Z, et al. High-Resolution Image Inpainting using Multi-Scale Neural Patch Synthesis[J]. arXiv preprint arXiv:1611.09969, 2016.

作者:xinzhangyanxiang 发表于2017/3/19 18:25:44 原文链接
阅读:897 评论:0 查看评论

斯坦福机器学习笔记(二)

树的三种存储结构

$
0
0

6.2树的定义

之前我们一直在谈的是一对一的线性结构,可现实中,还有很多一对多的情况需要处理,所以我们需要研究这种一对多的数据结构----"树",考虑它的各种特性,来解决我们在编程中碰到的相关问题。
树(Tree)是n(n>=0)个结点的有限集。n=0时称为空树。在任意一棵非空树中:(1)有且仅有一个特定的称为根(Root)的结点;(2)当n>1时,其余结点可分为m(m>O)个互不相交的有限集T1、T2、……、 Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree),如图6-2-1所示。

树的定义其实就是我们在讲解栈时提到的递归的方法。也就是在树的定义之中还用到了树的概念,这是一种比较新的定义方法。图6-2-2的子树T1和子树T2就是根结点A的子树。当然,D、G、H、I组成的树又是B为结点的子树,E、J组成的树是C为结点的子树。

对于树的定义还需要强调两点:
1.n>0时根结点是唯一的,不可能存在多个根结点,别和现实中的大树混在一起,现实中的树有很多根须,那是真实的树,数据结构中的树是只能有一个根结点。
2.m>0时,子树的个数没有限制,但它们一定是互不相交的。像图6-2-3中的两个结构就不符合树的定义,因为它们都有相交的子树。

6.2.1 结点分类

树的结点包含一个数据元素及若干指向其子树的分支。结点拥有的子树数称为结点的度(Degree)度为0的结点称为叶结点(Leaf)或终端结点;度不为0的结点称为非终端结点或分支结点。除根结点之外,分支结点也称为内部结点。树的度是树内各结点的度的最大值。如图6-2-4所示,因为这棵树结点的度的最大值是结点D的度,为3,所以树的度也为3。

6.2.2 结点间关系

结点的子树的根称为该结点的孩子(Child),相应地,该结点称为孩子的双亲(Parent)。恩,为什么不是父或母,叫双亲呢?对于结点来说其父母同体,唯一的一个,所以只能把它称为双亲了。同一个双亲的孩子之间互称兄弟(Sibling)结点的祖先是从根到该结点所经分支上的所有结点。所以对于H来说,D、B、A都是它的祖先。反之,以某结点为根的子树中的任一结点都称为该结点的子孙。B的子孙有D、G、H、I,如图6-2-5所示。

6.2.3 树的其他相关概念

结点的层次(Level)从根开始定义起,根为第一层,根的孩子为第二层。若某结点在第l层,则其子树的根就在第1+1层。其双亲在同一层的结点直为堂兄弟。显然图6-2-6中的D、E、F是堂兄弟,而G、H、I、J也是。树中结点的最大层次称为树的深度(Depth)或高度,当前树的深度为4。

如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树为有序树,否则称为无序树。
森林(Forest)是m(m>0)棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。对于图6-2-1中的树而言,图6-2-2中的两棵子树其实就可以理解为森林。
对比线性表与树的结构,它们有很大的不同,如图6-2-7所示。

6.4树的存储结构

说到存储结构,就会想到我们前面章节讲过的顺序存储和链式存储两种结构。
先来看看顺序存储结构,用一段地址连续的存储单元依次存储线性表的数据元素。这对于线性表来说是很自然的,对于树这样一多对的结构呢?
树中某个结点的孩子可以有多个,这就意味着,无论按何种顺序将树中所有结点存储到数组中,结点的存储位置都无法直接反映逻辑关系,你想想看,数据元素挨个的存储,谁是谁的双亲,谁是谁的孩子呢?简单的顺序存储结构是不能满足树的实现要求的。
不过充分利用顺序存储和链式存储结构的特点,完全可以实现对树的存储结构的表示。我们这里要介绍三种不同的表示法:双亲表示法、孩子表示法、孩子兄弟表示法

6.4.1 双亲表示法

我们人可能因为种种原因,没有孩子,但无论是谁都不可能是从石头里蹦出来的,孙悟空显然不能算是人,所以是人一定会有父母。树这种结构也不例外,除了根结点外,其余每个结点,它不一定有孩子,但是一定有且仅有一个双亲。
我们假设以一组连续空间存储树的结点,同时在每个结点中,附设一个指示器指示其双亲结点到链表中的位置。也就是说,每个结点除了知道自己是谁以外,还知道它的双亲在哪里。它的结点结构为表6-4-1所示。

其中data是数据域,存储结点的数据信息。而parent是指针域,存储该结点的双亲在数组中的下标。
由于根结点是没有双亲的,所以我们约定根结点的位置域设置为-1,这也就意味着,我们所有的结点都存有它双亲的位置。如图6-4-1中的树结构和表6-4-2中的树双亲表示所示。

这样的存储结构,我们可以根据结点的parent指针很容易找到它的双亲结点,所用的时间复杂度为O(1),直到parent为-1时,表示找到了树结点的根。可如果我们要知道结点的孩子是什么,对不起,请遍历整个结构才行。
这真是麻烦,能不能改进一下呢?
当然可以。我们增加一个结点最左边孩子的域,不妨叫它长子域,这样就可以很容易得到结点的孩子。如果没有孩子的结点,这个长子域就设置为-1,如表6-4-3所示。(表中下标为0的firstchild应该为1)

对于有0个或1个孩子结点来说,这样的结构是解决了要找结点孩子的问题了。甚至是有2个孩子,知道了长子是谁,另一个当然就是次子了。
另外一个问题场景,我们很关注各兄弟之间的关系,双亲表示法无法体现这样的关系,那我们怎么办?嗯,可以增加一个右兄弟域来体现兄弟关系,也就是说,每一个结点如果它存在右兄弟,则记录下右兄弟的下标。同样的,如果右兄弟不存在,则赋值为-1 ,如表6-4-4所示。

但如果结点的孩子很多,超过了2个。我们又关注结点的双亲、又关注结点的孩子、还关注结点的兄弟,而且对时间遍历要求还比较高,那么我们还可以把此结构扩展为有双亲域、长子域、再有右兄弟域。存储结构的设计是一个非常灵活的过程。一个存储结构设计得是否合理,取决于基于该存储结构的运算是否适合、是否方便,时间复杂度好不好等。注意也不是越多越好,有需要时再设计相应的结构。就像再好昕的音乐,不停反复听上千遍也会腻味,再好看的电影,一段时间反复看上百遍,也会无趣,你们说是吧?

6.4.2 孩子表示法

换一种完全不同的考虑方法 . 由于树中每个结点可能有多棵子树,可以考虑用多重链表,即每个结点有多个指针域,其中每个指针指向一棵子树的根结点,我们把这种方法叫做多重链表表示法。不过,树的每个结点的度,也就是它的孩子个数是不同的。所以可以设计两种方案来解决。
方案一
一种是指针域的个数就等于树的度,复习一下,树的度是树各个结点度的最大值。其结构如表6-4-5所示。

其中data是数据域。childl到childd是指针域,用来指向该结点的孩子结点。
对于图6-4-1的树来说,树的度是3,所以我们的指针域的个数是3,这种方法实现如图6-4-2所示。

这种方法对于树中各结点的度相差很大时,显然是很浪费空间的,因为有很多的结点,它的指针域都是空的。不过如果树的各结点度相差很小时,那就意味着开辟的空间被充分利用了,这时存储结构的缺点反而变成了优点。
既然很多指针域都可能为空,为什么不按需分配空间呢。于是我们有了第二种方案。
方案二
第二种方案每个结点指针域的个数等于该结点的度,我们专门取一个位置来存储结点指针域的个数,其结构如表6-4-6所示。

其中data为数据域,degree为度域,也就是存储该结点的孩子结点的个数,child1到childd为指针域,指向该结点的各个孩子的结点。
对于图6-4-2的树来说,这种方法实现如图6-4-3所示。

这种方法克服了浪费空间的缺点,对空间利用率是很高了,但是由于各个结点的链表是不相同的结构,加上要维护结点的度的数值,在运算上就会带来时间上的损耗。
能否有更好的方法,既可以减少空指针的浪费又能使结点结构相同。
仔细观察,我们为了要遍历整棵树,把每个结点放到一个顺序存储结构的数组中是合理的,但每个结点的孩子有多少是不确定的,所以我们再对每个结点的孩子建立一个单链表体现它们的关系 。
这就是我们要讲的孩子表示法。具体办法是,把每个结点的孩子结点排列起来,以单链表作存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空。然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中,如图6-4-4所示。

为此,设计两种结点结构,一个是孩子链表的孩子结点,如表6-4-7所示。

其中child是数据域,用来存储某个结点在表头数组中的下标。next是指针域,用来存储指向某结点的下一个孩子结点的指针。
另一个是表头数组的表头结点,如表6-4-8所示。

其中data是数据域,存储某结点的数据信息。firstchild是头指针域,存储该结点的孩子链表的头指针。
这样的结构对于我们要查找某个结点的某个孩子,或者找某个结点的兄弟,只需要查找这个结点的孩子单链表即可。对于遍历整棵树也是很方便的,对头结点的数组循环即可。
但是,这也存在着问题,我如何知道某个结点的双亲是谁呢?比较麻烦,需要整棵树遍历才行,难道就不可以把双亲表示法和孩子表示法综合一下吗?当然是可以。如图6-4-5所示。

我们把这种方法称为双亲孩子表示法,应该算是孩子表示法的改进。

6.4.3 孩子兄弟表示法

刚才我们分别从双亲的角度和从孩子的角度研究树的存储结构,如果我们从树结点的兄弟的角度又会如何呢? 当然,对于树这样的层级结构来说,只研究结点的兄弟是不行的,我们观察后发现,任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。 因此,我们设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟。结点结构如表6-4-9所示。

其中data是数据域,firstchild为指针域,存储该结点的第一个孩子结点的存储地址,rightsib是指针域,存储该结点的右兄弟结点的存储地址。
对于图6-4-1的树来说,这种方法实现的示意图如图6-4-6所示。

这种表示法,给查找某个结点的某个孩子带来了方便,只需要通过firstchild找到此结点的长子,然后再通过长子结点的rightsib找到它的二弟,接着一直下去,直到找到具体的孩子。当然,如果想找某个结点的双亲,这个表示法也是有做陷的,那怎么办呢?
对,如果真的有必要,完全可以再增加一个parent指针域来解决快速查找双亲的问题,这里就不再细谈了。
其实这个表示法的最大好处是它把一棵复杂的树变成了一棵二叉树。我们把图6-4-6变变形就成了图6-4-7这个样子。

这样就可以充分利用二叉树的特性和算法来处理这棵树了。嗯?有人间,二叉树是什么?哈哈,别急,这正是我接下来要重点讲的内容。
引用《大话数据结构》作者:程杰
作者:smile_from_2015 发表于2017/3/19 19:46:38 原文链接
阅读:94 评论:0 查看评论

康托展开

$
0
0
康托展开

  康托展开的公式是 X=an*(n-1)!+an-1*(n-2)!+...+ai*(i-1)!+...+a2*1!+a1*0! 其中,ai为当前未出现的元素中是排在第几个(从0开始)。
  这个公式可能看着让人头大,最好举个例子来说明一下。例如,有一个数组 s = ["A", "B", "C", "D"],它的一个排列 s1 = ["D", "B", "A", "C"],现在要把 s1 映射成 X。n 指的是数组的长度,也就是4,所以
X(s1) = a4*3! + a3*2! + a2*1! + a1*0!
关键问题是 a4、a3、a2 和 a1 等于啥?
a4 = "D" 这个元素在子数组 ["D", "B", "A", "C"] 中是第几大的元素。"A"是第0大的元素,"B"是第1大的元素,"C" 是第2大的元素,"D"是第3大的元素,所以 a4 = 3。
a3 = "B" 这个元素在子数组 ["B", "A", "C"] 中是第几大的元素。"A"是第0大的元素,"B"是第1大的元素,"C" 是第2大的元素,所以 a3 = 1。
a2 = "A" 这个元素在子数组 ["A", "C"] 中是第几大的元素。"A"是第0大的元素,"C"是第1大的元素,所以 a2 = 0。
a1 = "C" 这个元素在子数组 ["C"] 中是第几大的元素。"C" 是第0大的元素,所以 a1 = 0。(因为子数组只有1个元素,所以a1总是为0)
所以,X(s1) = 3*3! + 1*2! + 0*1! + 0*0! = 20


A B C | 0
A C B | 1
B A C | 2
B C A | 3
C A B | 4
C B A | 5

康托展开表示的是当前排列在n个不同元素的全排列中的名次。比如213在这3个数所有排列中排第3。

 

那么,对于n个数的排列,康托展开为:

 

其中表示第i个元素在未出现的元素中排列第几。举个简单的例子:

 

对于排列4213来说,4在4213中排第3,注意从0开始,2在213中排第1,1在13中排第0,3在3中排第0,即:

 

,这样得到4213在所有排列中排第ans=20

 

代码实现:(从0开始计数)

#include <iostream>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long LL;
LL fact[20];
void getFact(int n)
{
	fact[0]=1;
	for(int i=1;i<n;i++)
	{
		fact[i]=fact[i-1]*i;
	}
}
//康托展开,给出排列方式,求序号(从0开始) 
LL Cantor(int a[],int n)
{
	LL ans=0;
	for(int i=0;i<n;i++)
	{
		int cnt=0;
		for(int j=i+1;j<n;j++)
			if(a[j]<a[i])
				cnt++;
		ans+=cnt*fact[n-i-1];
	}
	return ans;
}

int main()
{
	getFact(20);
	int a[]={4,2,1,3};
	cout<<Cantor(a,4)<<endl;
	
	return 0;
}


通过康托逆展开生成全排列

  如果已知 s = ["A", "B", "C", "D"],X(s1) = 20,能否推出 s1 = ["D", "B", "A", "C"] 呢?
  因为已知 X(s1) = a4*3! + a3*2! + a2*1! + a1*0! = 20,所以问题变成由 20 能否唯一地映射出一组 a4、a3、a2、a1?如果不考虑 ai 的取值范围,有
3*3! + 1*2! + 0*1! + 0*0! = 20
2*3! + 4*2! + 0*1! + 0*0! = 20
1*3! + 7*2! + 0*1! + 0*0! = 20
0*3! + 10*2! + 0*1! + 0*0! = 20
0*3! + 0*2! + 20*1! + 0*0! = 20
等等。但是满足 0 <= ai <= n-1 的只有第一组。可以使用辗转相除的方法得到 ai,如下图所示:

知道了a4、a3、a2、a1的值,就可以知道s1[0] 是子数组["A", "B", "C", "D"]中第3大的元素 "D",s1[1] 是子数组 ["A", "B", "C"] 中第1大的元素"B",s1[2] 是子数组 ["A", "C"] 中第0大的元素"A",s[3] 是子数组 ["C"] 中第0大的元素"C",所以s1 = ["D", "B", "A", "C"]。
这样我们就能写出一个函数 Permutation3(),它可以返回  s 的第 m 个排列。

康托展开的逆运算:就是根据某个排列的在总的排列中的名次来确定这个排列。比如:


 

求1234所有排列中排第20的是啥,那么就利用辗转相除法确定康托展开中的系数,然后每次输出当前未出现过的第个元素。

 

代码实现康托展开逆运算:

#include <iostream>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long LL;
LL fact[20];
void getFact(int n)
{
	fact[0]=1;
	for(int i=1;i<n;i++)
	{
		fact[i]=fact[i-1]*i;
	}
}

//1-m组成的第n个排列, 
void CntorInver(LL n,LL m)
{
	
	//n--;//从1开始计数则-- 
	vector<int>v;
	vector<int>a;
	for(int i=1;i<=m;i++)
		v.push_back(i);
	for(int i=m;i>0;i--)
	{
		LL r = n % fact[i-1];
		LL t = n / fact[i-1];
		n = r;
		sort(v.begin(),v.end());
		a.push_back(v[t]);
		v.erase(v.begin()+t);
	}
	vector<int>::iterator it;
	for(it = a.begin();it!=a.end();it++)
		cout<<*it;
	cout<<endl;
}
int main()
{
	getFact(20);
	int a[]={4,2,1,3};
	cout<<Cantor(a,4)<<endl;
	CntorInver(20,4);
	return 0;
}


参考博客:

http://blog.csdn.net/zhongkeli/article/details/6966805

http://blog.csdn.net/acdreamers/article/details/7982067


作者:hurmishine 发表于2017/3/19 20:15:41 原文链接
阅读:88 评论:0 查看评论

Linux下安装Nginx完整教程及常见错误解决方案

$
0
0

1.Nginx安装环境

Nginx是C语言开发,建议在linux上运行,本教程使用Centos7.0作为安装环境.
1)gcc
安装nginx需要先将官网下载的源码进行编译,编译依赖gcc环境,如果没有gcc环境,需要安装gcc
需要执行的命令:yum install gcc-c++ 
2)PCRE
PCRE(Perl Compatible Regular Expressions)是一个Perl库,包括 perl 兼容的正则表达式库。nginx的http模块使用pcre来解析正则表达式,所以需要在linux上安装pcre库。
需要执行的命令:yum install -y pcre pcre-devel
3)zlib
zlib库提供了很多种压缩和解压缩的方式,nginx使用zlib对http包的内容进行gzip,所以需要在linux上安装zlib库。
需要运行的命令:yum install -y zlib zlib-devel
4)openssl
OpenSSL 是一个强大的安全套接字层密码库,囊括主要的密码算法、常用的密钥和证书封装管理功能及SSL协议,并提供丰富的应用程序供测试或其它目的使用。
nginx不仅支持http协议,还支持https(即在ssl协议上传输http),所以需要在linux安装openssl库。
需要运行的命令:yum install -y openssl openssl-devel

2.编译安装

   将nginx-1.8.0.tar.gz(地址:Nginx-1.8.0.tar.gz)拷贝至linux服务器.在安装的时候我会安装在/usr/local目录下,所以将Nginx的tar包拷贝到usr/local目录下.可以使用winSCP软件将tar包拷贝到local目录下.


1)解压:

使用命令:tar -zxvf nginx-1.8.0.tar.gz,将Nginx进行解压.


解压成功:



2)配置configure:

在nginx-1.8.0目录下运行如下命令:

./configure \
--prefix=/usr/local/nginx \
--pid-path=/var/run/nginx/nginx.pid \
--lock-path=/var/lock/nginx.lock \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--with-http_gzip_static_module \
--http-client-body-temp-path=/var/temp/nginx/client \
--http-proxy-temp-path=/var/temp/nginx/proxy \
--http-fastcgi-temp-path=/var/temp/nginx/fastcgi \
--http-uwsgi-temp-path=/var/temp/nginx/uwsgi \
--http-scgi-temp-path=/var/temp/nginx/scgi


3)编译安装

执行make命令


执行make stall命令


安装成功查看安装目录



4)运行Nginx

进入到sbin目录下,执行./nginx命令.


5)查看进程


3.测试Nginx是否运行成功

Nginx的端口是80,所以在浏览器上运行http:[linux的ip地址]即可,如果运行成功,出现如下界面


如果没有出现下面的界面,表示远程连接没有成功,如果linux的进程已经启动,尝试着将linux的防火墙关闭,运行如下命令,关闭防火墙,然后查看一下防火墙的状态.

执行完这个操作以后,再查看一下在本地是否能连上Nginx.

遇到的问题:

1.[emerg]mkdir()"/var/temp/nginx/client" failed(2:No such file or directory)

解决方法:
查看了一下是由于没有Nginx/client的目录.缺少对应的文件,建立相应的文件就好.
2.nginx/logs/nginx.pid" failed (2: No such file or directory)


解决方法:
重新编译(make,make install),安装就好.

总结:

   在linux上安装Nginx,其实是很简单的,自己在装的时候遇到了一些问题,在查找解决方法的时候,学习了一些基础的linux命令,掌握一些基础的linux总的来说,还是很好的.

作者:u013086062 发表于2017/3/19 21:16:41 原文链接
阅读:73 评论:0 查看评论

CSDN日报20170319——《人工智能风口, Python 程序员的狂欢与企业主的哀嚎》

$
0
0

【程序人生】人工智能风口, Python 程序员的狂欢与企业主的哀嚎

作者:赖勇浩

人工智能风口有多火?估计很多人已经感受到了,我在这里引用一下新智元的报道:

“2017年短短不到三个月的时间,国内AI获投项目已有36个,千万级别融资占据半数以上。”

嗯,就是那么霸道。两会刚刚结束,“人工智能”首次被列入政府工作报告,随之而来的是人工智能板块领跑大盘涨势,无疑,这一切将刺激人工智能在多个领域的全面发展。


【深度学习】NeuralFinder :集成人工生命和遗传算法自动发现神经网络最优结构

作者:张俊林

从 16 年年中开始,我们开始思考最优的深度神经网络结构自动发现的问题,并在业余时间开始逐步做些探索性的实验。当时的出发点其实很简单:对于解决某个机器学习任务,目前的常规做法是通过算法研发人员分析问题特性,并不断设计修改试探深度神经网络的结构,找到最适合解决手头问题的网络结构,然后通过不断调参来获得解决问题的最优网络结构及其对应的参数。很明显,这里需要耗费研发人员大量的精力,大家知道,AI在各个行业逐步辅助和替代人是个大的发展趋势,那么自然就产生一个问题:在探索解决手头问题的深度神经网络结构方面既然耗费了大量的精力,那么这个工作能不能让机器自动完成呢?也就是这项工作的初衷是代替深度学习研发工程师的日常工作。


【编程语言】Swift 中的错误处理

作者:LeoMobileDeveloper

任何代码都会发生错误,这些错误有些是可以补救的,有些则只能让程序崩溃。良好的错误处理能够让你的代码健壮性提高,提高程序的稳定性。


【系统运维】网络虚拟化( SDN , NFV.. )和企业骨干网的演化

作者:赵亚

在描述企业骨干网(大型企业的内网)之前,我得先来解释一下互联网路由的层次问题,理解了这个,你才能理解企业内网的构建规则,我还是以问题开始吧。


【云计算】OpenStack 实现技术分解 (5) 应用开发 — 使用 OpenStackClients 进行二次开发

作者:JmilkFan

OpenStack 为用户提供了三种操作方式, Web界面/CLI/RESTAPI, 实际上前两者是对 RESTAPI 做了两种不同形式的包装, 使用户可以通过网页或者指令行的方式来调用 RESTAPI 接口.

本篇博文主要记录了 使用 OpenStackClients (OSC 命令行客户端) 项目所提供了Python Bindings API 来进行二次开发的技巧, 以及实现一个启动虚拟机并部署 Workpass+MySQL 自动化脚本的 Demo. 源码详见 GitHub:openstackclient-api-demo


【Android 开发】 Android 热修复学习之旅 —— HotFix 完全解析

作者:首席套路管

QQ 空间热修复方案基于 Android dex 分包基础之上,简单概述 Android dex 分包的原理就是:就是把多个 dex 文件塞入到 app 的 classloader 之中,但是 Android dex 拆包方案中的类是没有重复的,如果 classes.dex 和 classes1.dex 中有重复的类,当 classes.dex 和 classes1.dex 中都具有同一个类的时候,那么 classloader 会选择加载哪个类呢?这要从 classloader 的源码入手,加载类是通过 classloader 的 loadClass 方法实现的,所以我们看一下 loadClass 的源码。


【数据库】Thinking in SQL 系列之六:数据挖掘 Apriori 关联分析再现啤酒尿布神话

作者:niulity

说起数据挖掘机器学习,印象中很早就听说过关于啤酒尿布的神话,这个问题经常出现在数据仓库相关的文章中,由此可见啤酒尿布问题对数据挖掘领域影响的深远程度。先看看它的成因:“啤酒与尿布”的故事产生于 20 世纪 90 年代的美国沃尔玛超市中,沃尔玛的超市管理人员分析销售数据时发现了一个令人难于理解的现象:在某些特定的情况下,“啤酒”与“尿布”两件看上去毫无关系的商品会经常出现在同一个购物篮中,这种独特的销售现象引起了管理人员的注意,经过后续调查发现,这种现象出现在年轻的父亲身上,买尿布的同时经常顺便带一瓶啤酒回家。


关注专栏【CSDN 日报】,获取最新及往期内容。

作者:blogdevteam 发表于2017/3/19 21:26:58 原文链接
阅读:942 评论:0 查看评论

【面试题】剑指Offer-28-字符串的全排列

$
0
0

题目概述

昨天做了一套CVTE的面试题,最后一个题目就是字符串的全排列。做过剑指Offer的童鞋一眼就可以看出这是剑指Offer-28题

原题目,一点都没变


题目解法

把字符串看成两个部分,第一个部分为第一个字符,剩下的是后面的字符

首先将所有可能出现在第一个位置的字符,将这些字符在每次循环的时候和第一个字符交换、

先固定一个字符,求后面字符的排列

然后将后面的字符按照前面的方法分成两部分,递归

代码实现

//面试题28:字符串的全排列
//pstr表示要打印的字符串,pbegin表示从该位置开始交换字符
void _Per(char* pstr, size_t start, size_t end)
{
	//要交换的字符指针已经到末尾,此时可以打印pstr字符串了
	if (start == end - 1)
	{
		printf("%s\n", pstr);
		return;
	}

	//注意,这里pCh不能初始为pbegin+1
	for (size_t index = start; index != end; ++index)
	{
		//依次交换后面的一个字符
		std::swap(pstr[start], pstr[index]);
		//进行递归
		_Per(pstr, start + 1, end);
		//将该字符还原
		std::swap(pstr[start], pstr[index]);
	}
}

void Per(char* pstr)
{
	if (pstr == NULL)
		return;

	_Per(pstr, 0, strlen(pstr));
}


void TestPer()
{
	char str[] = "abcd";
	Per(str);
	cout << endl;
}

几个注意的地方

1、cout<<*pstr; 只能打印当前的一个字符,并不能打印字符串;还是用printf("%s",pstr);

2、原书中用的都是指针,原书中交换是手动交换的,而我这里用下标来表示,用偏移加解引用比较方便点,不易出错

3、std::swap交换要注意,不能写成交换两个指针了

网上的另一种解法

用STL提供的算法
sort和next_permutation可以快速的求出全排列

代码实现

#include<algorithm>

void Per(char *str)
{
	int len = strlen(str);
	sort(str, str + len);

	//传入迭代器区间
	while (next_permutation(str, str + len))
	{
		printf("%s ,", str);
	}
}

void TestPer()
{
	char str[] = "abcd";
	Per(str);
	cout << endl;
}
作者:qq_31828515 发表于2017/3/19 21:35:42 原文链接
阅读:75 评论:0 查看评论

从代码层读懂HashMap的实现原理

$
0
0

概述

 Hashmap继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。它的key、value都可以为null,映射不是有序的。      
Hashmap不是同步的,如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap。

Map map = Collections.synchronizedMap(new HashMap());

HashMap 中两个重要的参数:“初始容量” 和 “加载因子”。
容量: 是哈希表中桶的数量,初始容量 只是哈希表在创建时的容量
加载因子: 是哈希表在其容量自动增加之前可以达到多满的一种尺度(默认0.75)。 
当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构,桶数X2)。
加载因子越大,填满的元素越多,好处是,空间利用率高了,但:冲突的机会加大了.反之,加载因子越小,填满的元素越少, 好处是:冲突的机会减小了,但:空间浪费多了.

HashMap数据结构

Hashmap本质是数组加链表。通过key的hashCode来计算hash值的,只要hashCode相同,计算出来的hash值就一样,然后再计算出数组下标,如果多个key对应到同一个下标,就用链表串起来,新插入的在前面。

这里写图片描述

先来看看HashMap中Entry类的代码:

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    // 指向下一个节点
    Entry<K,V> next;
    final int hash;
    // 构造函数。
    // 输入参数包括"哈希值(h)", "键(k)", "值(v)", "下一节点(n)"
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }
    public final K getKey() {
        return key;
    }
    public final V getValue() {
        return value;
    }
    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }
    // 判断两个Entry是否相等
    // 若两个Entry的“key”和“value”都相等,则返回true。
    // 否则,返回false
    public final boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry e = (Map.Entry)o;
        Object k1 = getKey();
        Object k2 = e.getKey();
        if (k1 == k2 || (k1 != null && k1.equals(k2))) {
            Object v1 = getValue();
            Object v2 = e.getValue();
            if (v1 == v2 || (v1 != null && v1.equals(v2)))
                return true;
        }
        return false;
    }
    // 实现hashCode()
    public final int hashCode() {
        return (key==null   ? 0 : key.hashCode()) ^
               (value==null ? 0 : value.hashCode());
    }
    public final String toString() {
        return getKey() + "=" + getValue();
    }
    // 当向HashMap中添加元素时,绘调用recordAccess()。
    // 这里不做任何处理
    void recordAccess(HashMap<K,V> m) {
    }
    // 当从HashMap中删除元素时,绘调用recordRemoval()。
    // 这里不做任何处理
    void recordRemoval(HashMap<K,V> m) {
    }
}

可以看出HashMap就是一个Entry数组,Entry对象中包含了键和值两个属性。

HashMap源码分析

HashMap共有4个构造函数,如下:

  • HashMap() 构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。
  • HashMap(int initialCapacity) 构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。
  • HashMap(int initialCapacity, float loadFactor) 构造一个带指定初始容量和加载因子的空
    HashMap。
  • HashMap(Map<  extends K, extends V> m) 构造一个映射关系与指定 Map 相同的新 HashMap。

HashMap提供的API方法:

  • void clear() 从此映射中移除所有映射关系。
  • Object clone() 返回此 HashMap 实例的浅表副本:并不复制键和值本身。
  • boolean containsKey(Object key) 如果此映射包含对于指定键的映射关系,则返回 true。
  • boolean containsValue(Object value) 如果此映射将一个或多个键映射到指定值,则返回 true。
  • Set entrySet() 返回此映射所包含的映射关系的 Set<Map.Entry> 视图。
  • V get(Object key) 返回指定键所映射的值;如果对于该键来说,此映射不包含任何映射关系,则返回 null。
  • boolean isEmpty() 如果此映射不包含键-值映射关系,则返回 true。
  • Set keySet() 返回此映射中所包含的键的 Set<K> 视图。
  • V put(K key, V value) 在此映射中关联指定值与指定键。
  • void  putAll(Map< extends K, extends V> m)
    将指定映射的所有映射关系复制到此映射中,这些映射关系将替换此映射目前针对指定映射中所有键的所有映射关系。
  • V remove(Object key) 从此映射中移除指定键的映射关系(如果存在)。
  • int  size() 返回此映射中的键-值映射关系数。
  • Collection values() 返回此映射所包含的值的 Collection 视图。

HashMap源码:

package java.util;
import java.io.*;
public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable
{
    //  默认的初始容量(容量为HashMap中桶的数目)是16,且实际容量必须是2的整数次幂。 
    static final int DEFAULT_INITIAL_CAPACITY = 16;

    // 最大容量(必须是2的幂且小于2的30次方,传入容量过大将被这个值替换)
    static final int MAXIMUM_CAPACITY = 1 << 30;

    // 默认加载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    // 存储数据的Entry数组,长度是2的幂。
    // HashMap是采用拉链法实现的,每一个Entry本质上是一个单向链表
    transient Entry[] table;

    // HashMap的大小,它是HashMap保存的键值对的数量
    transient int size;

    // HashMap的阈值,用于判断是否需要调整HashMap的容量(threshold = 容量*加载因子)
    int threshold;

    // 加载因子实际大小
    final float loadFactor;

    // HashMap被改变的次数
    transient volatile int modCount;

    // 指定“容量大小”和“加载因子”的构造函数
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        // HashMap的最大容量只能是MAXIMUM_CAPACITY
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        // 找出“大于initialCapacity”的最小的2的幂
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;
        // 设置“加载因子”
        this.loadFactor = loadFactor;
        // 设置“HashMap阈值”,当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍。
        threshold = (int)(capacity * loadFactor);
        // 创建Entry数组,用来保存数据
        table = new Entry[capacity];
        init();
    }

    // 指定“容量大小”的构造函数
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    // 默认构造函数。
    public HashMap() {
        // 设置“加载因子”
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        // 设置“HashMap阈值”,当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍。
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
        // 创建Entry数组,用来保存数据
        table = new Entry[DEFAULT_INITIAL_CAPACITY];
        init();
    }

    // 包含“子Map”的构造函数
    public HashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        // 将m中的全部元素逐个添加到HashMap中
        putAllForCreate(m);
    }

    static int hash(int h) {
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

    // 返回索引值
    // h & (length-1)保证返回值的小于length
    static int indexFor(int h, int length) {
        return h & (length-1);
    }

    public int size() {
        return size;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    // 获取key对应的value
    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        // 获取key的hash值
        int hash = hash(key.hashCode());
        // 在“该hash值对应的链表”上查找“键值等于key”的元素
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }

    // 获取“key为null”的元素的值
    // HashMap将“key为null”的元素存储在table[0]位置!
    private V getForNullKey() {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

    // HashMap是否包含key
    public boolean containsKey(Object key) {
        return getEntry(key) != null;
    }

    // 返回“键为key”的键值对
    final Entry<K,V> getEntry(Object key) {
        // 获取哈希值
        // HashMap将“key为null”的元素存储在table[0]位置,“key不为null”的则调用hash()计算哈希值
        int hash = (key == null) ? 0 : hash(key.hashCode());
        // 在“该hash值对应的链表”上查找“键值等于key”的元素
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

    // 将“key-value”添加到HashMap中
    public V put(K key, V value) {
        // 若“key为null”,则将该键值对添加到table[0]中。
        if (key == null)
            return putForNullKey(value);
        // 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            // 若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出!
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        // 若“该key”对应的键值对不存在,则将“key-value”添加到table中
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

    // putForNullKey()的作用是将“key为null”键值对添加到table[0]位置
    private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        // 这里的完全不会被执行到!
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }

    // 创建HashMap对应的“添加方法”,
    // 它和put()不同。putForCreate()是内部方法,它被构造函数等调用,用来创建HashMap
    // 而put()是对外提供的往HashMap中添加元素的方法。
    private void putForCreate(K key, V value) {
        int hash = (key == null) ? 0 : hash(key.hashCode());
        int i = indexFor(hash, table.length);
        // 若该HashMap表中存在“键值等于key”的元素,则替换该元素的value值
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                e.value = value;
                return;
            }
        }
        // 若该HashMap表中不存在“键值等于key”的元素,则将该key-value添加到HashMap中
        createEntry(hash, key, value, i);
    }

    // 将“m”中的全部元素都添加到HashMap中。
    // 该方法被内部的构造HashMap的方法所调用。
    private void putAllForCreate(Map<? extends K, ? extends V> m) {
        // 利用迭代器将元素逐个添加到HashMap中
        for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) {
            Map.Entry<? extends K, ? extends V> e = i.next();
            putForCreate(e.getKey(), e.getValue());
        }
    }

    // 重新调整HashMap的大小,newCapacity是调整后的单位
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        // 新建一个HashMap,将“旧HashMap”的全部元素添加到“新HashMap”中,
        // 然后,将“新HashMap”赋值给“旧HashMap”。
        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable);
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
    }

    // 将HashMap中的全部元素都添加到newTable中
    void transfer(Entry[] newTable) {
        Entry[] src = table;
        int newCapacity = newTable.length;
        for (int j = 0; j < src.length; j++) {
            Entry<K,V> e = src[j];
            if (e != null) {
                src[j] = null;
                do {
                    Entry<K,V> next = e.next;
                    int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                } while (e != null);
            }
        }
    }

    // 将"m"的全部元素都添加到HashMap中
    public void putAll(Map<? extends K, ? extends V> m) {
        // 有效性判断
        int numKeysToBeAdded = m.size();
        if (numKeysToBeAdded == 0)
            return;
        // 计算容量是否足够,
        // 若“当前实际容量 < 需要的容量”,则将容量x2。
        if (numKeysToBeAdded > threshold) {
            int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
            if (targetCapacity > MAXIMUM_CAPACITY)
                targetCapacity = MAXIMUM_CAPACITY;
            int newCapacity = table.length;
            while (newCapacity < targetCapacity)
                newCapacity <<= 1;
            if (newCapacity > table.length)
                resize(newCapacity);
        }
        // 通过迭代器,将“m”中的元素逐个添加到HashMap中。
        for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) {
            Map.Entry<? extends K, ? extends V> e = i.next();
            put(e.getKey(), e.getValue());
        }
    }

    // 删除“键为key”元素
    public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }

    // 删除“键为key”的元素
    final Entry<K,V> removeEntryForKey(Object key) {
        // 获取哈希值。若key为null,则哈希值为0;否则调用hash()进行计算
        int hash = (key == null) ? 0 : hash(key.hashCode());
        int i = indexFor(hash, table.length);
        Entry<K,V> prev = table[i];
        Entry<K,V> e = prev;
        // 删除链表中“键为key”的元素
        // 本质是“删除单向链表中的节点”
        while (e != null) {
            Entry<K,V> next = e.next;
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }
        return e;
    }

    // 删除“键值对”
    final Entry<K,V> removeMapping(Object o) {
        if (!(o instanceof Map.Entry))
            return null;
        Map.Entry<K,V> entry = (Map.Entry<K,V>) o;
        Object key = entry.getKey();
        int hash = (key == null) ? 0 : hash(key.hashCode());
        int i = indexFor(hash, table.length);
        Entry<K,V> prev = table[i];
        Entry<K,V> e = prev;
        // 删除链表中的“键值对e”
        // 本质是“删除单向链表中的节点”
        while (e != null) {
            Entry<K,V> next = e.next;
            if (e.hash == hash && e.equals(entry)) {
                modCount++;
                size--;
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }
        return e;
    }

    // 清空HashMap,将所有的元素设为null
    public void clear() {
        modCount++;
        Entry[] tab = table;
        for (int i = 0; i < tab.length; i++)
            tab[i] = null;
        size = 0;
    }

    // 是否包含“值为value”的元素
    public boolean containsValue(Object value) {
    // 若“value为null”,则调用containsNullValue()查找
    if (value == null)
            return containsNullValue();
    // 若“value不为null”,则查找HashMap中是否有值为value的节点。
    Entry[] tab = table;
        for (int i = 0; i < tab.length ; i++)
            for (Entry e = tab[i] ; e != null ; e = e.next)
                if (value.equals(e.value))
                    return true;
    return false;
    }

    // 是否包含null值
    private boolean containsNullValue() {
    Entry[] tab = table;
        for (int i = 0; i < tab.length ; i++)
            for (Entry e = tab[i] ; e != null ; e = e.next)
                if (e.value == null)
                    return true;
    return false;
    }

    // 克隆一个HashMap,并返回Object对象
    public Object clone() {
        HashMap<K,V> result = null;
        try {
            result = (HashMap<K,V>)super.clone();
        } catch (CloneNotSupportedException e) {
            // assert false;
        }
        result.table = new Entry[table.length];
        result.entrySet = null;
        result.modCount = 0;
        result.size = 0;
        result.init();
        // 调用putAllForCreate()将全部元素添加到HashMap中
        result.putAllForCreate(this);
        return result;
    }

    // Entry是单向链表。
    // 它是 “HashMap链式存储法”对应的链表。
    // 它实现了Map.Entry 接口,即实现getKey(), getValue(), setValue(V value), equals(Object o), hashCode()这些函数
    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        // 指向下一个节点
        Entry<K,V> next;
        final int hash;
        // 构造函数。
        // 输入参数包括"哈希值(h)", "键(k)", "值(v)", "下一节点(n)"
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
        public final K getKey() {
            return key;
        }
        public final V getValue() {
            return value;
        }
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
        // 判断两个Entry是否相等
        // 若两个Entry的“key”和“value”都相等,则返回true。
        // 否则,返回false
        public final boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry)o;
            Object k1 = getKey();
            Object k2 = e.getKey();
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = getValue();
                Object v2 = e.getValue();
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            }
            return false;
        }
        // 实现hashCode()
        public final int hashCode() {
            return (key==null   ? 0 : key.hashCode()) ^
                   (value==null ? 0 : value.hashCode());
        }
        public final String toString() {
            return getKey() + "=" + getValue();
        }
        // 当向HashMap中添加元素时,绘调用recordAccess()。
        // 这里不做任何处理
        void recordAccess(HashMap<K,V> m) {
        }
        // 当从HashMap中删除元素时,绘调用recordRemoval()。
        // 这里不做任何处理
        void recordRemoval(HashMap<K,V> m) {
        }
    }

    // 新增Entry。将“key-value”插入指定位置,bucketIndex是位置索引。
    void addEntry(int hash, K key, V value, int bucketIndex) {
        // 保存“bucketIndex”位置的值到“e”中
        Entry<K,V> e = table[bucketIndex];
        // 设置“bucketIndex”位置的元素为“新Entry”,
        // 设置“e”为“新Entry的下一个节点”
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        // 若HashMap的实际大小 不小于 “阈值”,则调整HashMap的大小
        if (size++ >= threshold)
            resize(2 * table.length);
    }

    // 创建Entry。将“key-value”插入指定位置,bucketIndex是位置索引。
    // 它和addEntry的区别是:
    // (01) addEntry()一般用在 新增Entry可能导致“HashMap的实际容量”超过“阈值”的情况下。
    //   例如,我们新建一个HashMap,然后不断通过put()向HashMap中添加元素;
    // put()是通过addEntry()新增Entry的。
    //   在这种情况下,我们不知道何时“HashMap的实际容量”会超过“阈值”;
    //   因此,需要调用addEntry()
    // (02) createEntry() 一般用在 新增Entry不会导致“HashMap的实际容量”超过“阈值”的情况下。
    //   例如,我们调用HashMap“带有Map”的构造函数,它绘将Map的全部元素添加到HashMap中;
    // 但在添加之前,我们已经计算好“HashMap的容量和阈值”。也就是,可以确定“即使将Map中
    // 的全部元素添加到HashMap中,都不会超过HashMap的阈值”。
    //   此时,调用createEntry()即可。
    void createEntry(int hash, K key, V value, int bucketIndex) {
        // 保存“bucketIndex”位置的值到“e”中
        Entry<K,V> e = table[bucketIndex];
        // 设置“bucketIndex”位置的元素为“新Entry”,
        // 设置“e”为“新Entry的下一个节点”
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        size++;
    }

    // HashIterator是HashMap迭代器的抽象出来的父类,实现了公共了函数。
    // 它包含“key迭代器(KeyIterator)”、“Value迭代器(ValueIterator)”和“Entry迭代器(EntryIterator)”3个子类。
    private abstract class HashIterator<E> implements Iterator<E> {
        // 下一个元素
        Entry<K,V> next;
        // expectedModCount用于实现fast-fail机制。
        int expectedModCount;
        // 当前索引
        int index;
        // 当前元素
        Entry<K,V> current;
        HashIterator() {
            expectedModCount = modCount;
            if (size > 0) { // advance to first entry
                Entry[] t = table;
                // 将next指向table中第一个不为null的元素。
                // 这里利用了index的初始值为0,从0开始依次向后遍历,直到找到不为null的元素就退出循环。
                while (index < t.length && (next = t[index++]) == null)

            }
        }
        public final boolean hasNext() {
            return next != null;
        }
        // 获取下一个元素
        final Entry<K,V> nextEntry() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            Entry<K,V> e = next;
            if (e == null)
                throw new NoSuchElementException();
            // 注意!!!
            // 一个Entry就是一个单向链表
            // 若该Entry的下一个节点不为空,就将next指向下一个节点;
            // 否则,将next指向下一个链表(也是下一个Entry)的不为null的节点。
            if ((next = e.next) == null) {
                Entry[] t = table;
                while (index < t.length && (next = t[index++]) == null)

            }
            current = e;
            return e;
        }
        // 删除当前元素
        public void remove() {
            if (current == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            Object k = current.key;
            current = null;
            HashMap.this.removeEntryForKey(k);
            expectedModCount = modCount;
        }
    }
    // value的迭代器
    private final class ValueIterator extends HashIterator<V> {
        public V next() {
            return nextEntry().value;
        }
    }
    // key的迭代器
    private final class KeyIterator extends HashIterator<K> {
        public K next() {
            return nextEntry().getKey();
        }
    }
    // Entry的迭代器
    private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
        public Map.Entry<K,V> next() {
            return nextEntry();
        }
    }
    // 返回一个“key迭代器”
    Iterator<K> newKeyIterator()   {
        return new KeyIterator();
    }
    // 返回一个“value迭代器”
    Iterator<V> newValueIterator()   {
        return new ValueIterator();
    }
    // 返回一个“entry迭代器”
    Iterator<Map.Entry<K,V>> newEntryIterator()   {
        return new EntryIterator();
    }
    // HashMap的Entry对应的集合
    private transient Set<Map.Entry<K,V>> entrySet = null;
    // 返回“key的集合”,实际上返回一个“KeySet对象”
    public Set<K> keySet() {
        Set<K> ks = keySet;
        return (ks != null ? ks : (keySet = new KeySet()));
    }
    // Key对应的集合
    // KeySet继承于AbstractSet,说明该集合中没有重复的Key。
    private final class KeySet extends AbstractSet<K> {
        public Iterator<K> iterator() {
            return newKeyIterator();
        }
        public int size() {
            return size;
        }
        public boolean contains(Object o) {
            return containsKey(o);
        }
        public boolean remove(Object o) {
            return HashMap.this.removeEntryForKey(o) != null;
        }
        public void clear() {
            HashMap.this.clear();
        }
    }
    // 返回“value集合”,实际上返回的是一个Values对象
    public Collection<V> values() {
        Collection<V> vs = values;
        return (vs != null ? vs : (values = new Values()));
    }
    // “value集合”
    // Values继承于AbstractCollection,不同于“KeySet继承于AbstractSet”,
    // Values中的元素能够重复。因为不同的key可以指向相同的value。
    private final class Values extends AbstractCollection<V> {
        public Iterator<V> iterator() {
            return newValueIterator();
        }
        public int size() {
            return size;
        }
        public boolean contains(Object o) {
            return containsValue(o);
        }
        public void clear() {
            HashMap.this.clear();
        }
    }
    // 返回“HashMap的Entry集合”
    public Set<Map.Entry<K,V>> entrySet() {
        return entrySet0();
    }
    // 返回“HashMap的Entry集合”,它实际是返回一个EntrySet对象
    private Set<Map.Entry<K,V>> entrySet0() {
        Set<Map.Entry<K,V>> es = entrySet;
        return es != null ? es : (entrySet = new EntrySet());
    }
    // EntrySet对应的集合
    // EntrySet继承于AbstractSet,说明该集合中没有重复的EntrySet。
    private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        public Iterator<Map.Entry<K,V>> iterator() {
            return newEntryIterator();
        }
        public boolean contains(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry<K,V> e = (Map.Entry<K,V>) o;
            Entry<K,V> candidate = getEntry(e.getKey());
            return candidate != null && candidate.equals(e);
        }
        public boolean remove(Object o) {
            return removeMapping(o) != null;
        }
        public int size() {
            return size;
        }
        public void clear() {
            HashMap.this.clear();
        }
    }
    // java.io.Serializable的写入函数
    // 将HashMap的“总的容量,实际容量,所有的Entry”都写入到输出流中
    private void writeObject(java.io.ObjectOutputStream s)
        throws IOException
    {
        Iterator<Map.Entry<K,V>> i =
            (size > 0) ? entrySet0().iterator() : null;
        // Write out the threshold, loadfactor, and any hidden stuff
        s.defaultWriteObject();
        // Write out number of buckets
        s.writeInt(table.length);
        // Write out size (number of Mappings)
        s.writeInt(size);
        // Write out keys and values (alternating)
        if (i != null) {
            while (i.hasNext()) {
            Map.Entry<K,V> e = i.next();
            s.writeObject(e.getKey());
            s.writeObject(e.getValue());
            }
        }
    }
    private static final long serialVersionUID = 362498820763181265L;
    // java.io.Serializable的读取函数:根据写入方式读出
    // 将HashMap的“总的容量,实际容量,所有的Entry”依次读出
    private void readObject(java.io.ObjectInputStream s)
         throws IOException, ClassNotFoundException
    {
        // Read in the threshold, loadfactor, and any hidden stuff
        s.defaultReadObject();
        // Read in number of buckets and allocate the bucket array;
        int numBuckets = s.readInt();
        table = new Entry[numBuckets];
        init();  // Give subclass a chance to do its thing.
        // Read in size (number of Mappings)
        int size = s.readInt();
        // Read the keys and values, and put the mappings in the HashMap
        for (int i=0; i<size; i++) {
            K key = (K) s.readObject();
            V value = (V) s.readObject();
            putForCreate(key, value);
        }
    }
    // 返回“HashMap总的容量”
    int   capacity()     { return table.length; }
    // 返回“HashMap的加载因子”
    float loadFactor()   { return loadFactor;   }
}

主要代码分析:

  • public V get(Object key):

    如果key不为null,则先求的key的hash值,根据hash值找到在table中的索引,在该索引对应的单链表中查找是否有键值对的key与目标key相等,有就返回对应的value,没有则返回null。   如果key为null,则直接从哈希表的第一个位置table[0]对应的链表上查找。记住,key为null的键值对永远都放在以table[0]为头结点的链表中,当然不一定是存放在头结点table[0]中。

  • public V put(K key, V value)

    如果key不为null,则同样先求出key的hash值,根据hash值得出在table中的索引,而后遍历对应的单链表,如果单链表中存在与目标key相等的键值对,则将新的value覆盖旧的value,并将旧的value返回,如果找不到与目标key相等的键值对,或者该单链表为空,则将该键值对插入到改单链表的头结点位置(每次新插入的节点都是放在头结点的位置),该操作是有addEntry方法实现的,它的源码如下:

 void addEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex]; //如果要加入的位置有值,将该位置原先的值设置为新entry的next,也就是新entry链表的下一个节点
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        if (size++ >= threshold) //如果大于临界值就扩容
            resize(2 * table.length); //以2的倍数扩容
    }

参数bucketIndex就是indexFor函数计算出来的索引值,第2行代码是取得数组中索引为bucketIndex的Entry对象,第3行就是用hash、key、value构建一个新的Entry对象放到索引为bucketIndex的位置,并且将该位置原先的对象设置为新对象的next构成链表。第4行和第5行就是判断put后size是否达到了临界值threshold,如果达到了临界值就要进行扩容,HashMap扩容是扩为原来的两倍。

如果key为null,则将其添加到table[0]对应的链表中,由putForNullKey()实现。

 // putForNullKey()的作用是将“key为null”键值对添加到table[0]位置  
    private V putForNullKey(V value) {  
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {  
            if (e.key == null) {  
                V oldValue = e.value;  
                e.value = value;  
                e.recordAccess(this);  
                return oldValue;  
            }  
        }  
        // 如果没有存在key为null的键值对,则直接题阿见到table[0]处!  
        modCount++;  
        addEntry(0, null, value, 0);  
        return null;  
    } 

涉及到的resize扩容方法:

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable);//用来将原先table的元素全部移到newTable里面
        table = newTable;  //再将newTable赋值给table
        threshold = (int)(newCapacity * loadFactor);//重新计算临界值
    } 

  它新建了一个HashMap的底层数组,而后调用transfer方法,将就HashMap的全部元素添加到新的HashMap中(要重新计算元素在新的数组中的索引位置)。   扩容是需要进行数组复制的,非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

  • hash()
static int hash(int h) {
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
  • hash值找到对应索引
static int indexFor(int h, int length) {
        return h & (length-1);
    }

HashMap中则通过h&(length-1)的方法来代替取模,同样实现了均匀的散列,但效率要高很多,这也是HashMap对Hashtable的一个改进。   length为2的整数次幂的话,h&(length-1)就相当于对length取模,这样便保证了散列的均匀,同时也提升了效率。

说明:length为2的整数次幂的话,为偶数,这样length-1为奇数,奇数的最后一位是1,这样便保证了h&(length-1)的最后一位可能为0,也可能为1(这取决于h的值),即与后的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀性,而如果length为奇数的话,很明显length-1为偶数,它的最后一位是0,这样h&(length-1)的最后一位肯定为0,即只能为偶数,这样任何hash值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间。

作者:xiangzhihong8 发表于2017/3/19 22:25:26 原文链接
阅读:151 评论:0 查看评论

C和C++的区别 01.namespace

$
0
0

传统的C将所有全局变量编译到一个命名空间,不允许重名。但是C++是为了应对大型的软件设计而生的,所以命名自然多了许多。

为此C++开发了一种语法,叫命名空间,可以类比成厂家,C++把自己官方定义的系统函数放在了std(standard)空间下,我们使用using namespace std;这句话,则表示我们使用的是系统的这个命名空间。

我们也可以自己定义命名空间,格式是namespace 名字{};

如果我们已经选择了A厂家,但是想用个别的B厂家的东西怎么办呢?可以用:: 域作用符,就是两个冒号。

#include <iostream>
#include <stdio.h>

using namespace std;
// 不加这句,则需使用 :: 域作用符 std::cout

//命名空间的定义
namespace NameSpaceA
{
    int a = 0;
}

//嵌套定义
namespace NameSpaceB
{
    int a = 1;
    namespace NameSpaceC
    {
        struct Teacher
        {
            char name[10];
            int age;
        };
    }
}


int main()
{
    using namespace NameSpaceA;
    using NameSpaceB::NameSpaceC::Teacher;   //使用命名空间里面的变量

    printf("a = %d\n", a);              //是默认的空间    0
    printf("a = %d\n", NameSpaceB::a);  //使用里面的定义    1

    Teacher t1 = {"aaa", 3};

    printf("t1.name = %s\n", t1.name);
    printf("t1.age = %d\n", t1.age);

    return 0;

}
作者:happy_teemo 发表于2017/3/20 17:39:12 原文链接
阅读:95 评论:0 查看评论

架构设计:系统存储(25)——数据一致性与Paxos算法(下)

$
0
0

(接上文《架构设计:系统存储(24)——数据一致性与Paxos算法(中)》)

2-3. Paxos算法变种

上文(架构设计:系统存储(24)——数据一致性与Paxos算法(中))我们提到,Paxos算法的设计是为了解决多个Proposer针对一个具体提案产生的多个赋值,并最终帮助多个Proposer确定一个大家一致认可的赋值,最终达到一致性。从理论层面上说,如果多个Proposer一直不产生竞争起来,那么算法研究就没有意义。例如上文中提到的一个Proposer A的投票轮次编号始终从1000开始,其它Proposer的投票轮次编号始终从100开始,那样的话Paxos算法系统中其它Proposer发起的提案申请在第一轮就会被拒绝(在Proposer A的第一次提案申请到达Acceptor之前,可能会有其它Proposer发起的提案进入第二轮,但最终也会被大多数Acceptor决绝),所以这样的Proposer编号设定实际上就会让所有Proposer都听命于Proposer A的提案决断。

可是从实际应用的角度出发,却刚好相反——让Paxos算法过程尽可能少的产生竞争有利于提高算法效率。在Basic-Paxos算法中,任何一个提案要被通过都至少需要和单独的Acceptor通讯两次(这里还不考虑多Proposer抢占的问题),如果有N个Acceptor则至少进行 (N/2 + 1)× 2次网络通讯。如果在一个完整的Paxos算法中,网络通讯占去了相当的时间,那么Paxos算法的性能自然不会好。所以Paxos算法会产生了很多变种形式。本节我们就对一些主要的变种形式进行说明

2-4. Paxos变种:Multi-Paxos算法

介绍Basic-Paxos时我们只针对了一个议题形成多数派达成一致的值的算法过程进行了说明,无论有多个Proposer参与了抢占Acceptor赋值权的过程,无论为了形成这个多数派决议发起了多少轮Prepare投票申请,也无论Acceptor拒绝了多少次Acceptor赋值请求。所有这一切都只是为了处理一个议题,如:关于K的赋值是多少、关于你爸的名字是A还是B、关于下一次事件触发时间是在1000秒以后还是2000秒以后。

但实际业务场景往往不是这样的,在分布式存储环境中,拥有Paxos算法功能的整个系统需要在生命周期内对多种议题进行一致性处理,例如日志编号X的操作是将编号K的值赋为A还是赋为B;下一个日志编号X+1的操作,是将K的值赋为Y还是赋为W……

为了将Basic-Paxos算法用于实际应用中,Basic-Paxos算法有一种变种算法叫做Multi-Paxos,其中的“Multi”当然就代表在Paxos算法系统存活的整个生命周期需要处理的多个提案。Multi-Paxos算法的工作过程大致可以概括为选主保持、正式工作、重选恢复,本节我们就和大家一起讨论一下Multi-Paxos算法这几个工作步骤。

2-4-1. Multi-Paxos:选主与保持

Multi-Paxos算法核心过程的工作原则和Basic-Paxos算法核心工作原则是相同的,即多数派决定、Acceptor第一轮只接受更大的投票编号、Acceptor第二轮只接受和目前第一轮授权相同的投票编号。但是为了提高对每一个议题的判定效率,Multi-Paxos算法在第一投票过程中做了一个调整:首先通过一个完整的Paxos算法过程完成一次选主操作,从多个Proposer中选出一个Proposer Leader,待所有Proposer都确认这个Proposer Leader后,后续的带有业务性质的提案,都只由这个Proposer Leader发起和赋值。这样的做法至少有两个好处:

  • 其它Proposer在确定自己不是Proposer Leader后,就不再发起任何提案的投票申请,而只是接受Learner(最终结果学习者)传来的针对某个提案的最终赋值结果。而往往Learner和Proposer都存在于同一个进程中,所以这基本上消除了这些Proposer的网络负载。

  • 一旦Proposer Leader被选出,且后续的提案只由它发起和赋值。那么就不存在投票申请冲突的情况了——因为没有任何其它的Proposer和它形成竞争关系。Acceptor所接受的投票轮次编号也都由它提出,那么这些后续提案的第一阶段都可以跳过,而直接进入提案的赋值阶段。这样类似Basic-Paxos算法中要确定一个提案的赋值至少需要和每个Acceptor都通讯两次的情况就被削减成了只需要一次通讯。

请注意,Multi-Paxos算法的选主过程和上一篇文章(架构设计:系统存储(24)——数据一致性与Paxos算法(中))我们提到的直接决定一个起始投票编号最大的Proposer方式的本质区别是:前者是投票决定的,而后者是被固定的,一旦后者那个投票编号最大的Proposer宕机了,其它Proposer在提案投票过程中就会一直出现冲突。另外,由于多个Proposer中并没有Leader的概念,所以这些Proposer就算提案投票申请被拒,也会不停的再发起新一轮投票申请,而这样并不会减少网络通信压力。

这里写图片描述

要说明整个Multi-Paxos算法的可行性,就需要明确一个事实。当我们介绍Basic-Paxos算法时,为了读者能够腾出所有精力缕清它的工作步骤,所以将Paxos算法中的Proposer、Acceptor、Leaner等角色分开介绍。但实际情况是,分布式系统中的每个节点都同时具备这几种角色,例如工作在Server 1节点的Acceptor角色能够实时知晓这个节点上Proposer角色的工作状态;Proposer角色也实时知晓同一节点上Leaner角色对提案最终结果的学习状态。这也是为什么我们讨论Paxos算法时经常会将属于Leaner角色的部分公论讨论到Proposer角色中去的原因。

回到Multi-Paxos算法的选主过程来,这个过程就是一个完整的Basic-Paxos过程,这里就不再赘述了。主要需要说明的是保持方式,其它Proposer角色可以从Basic-Paxos过程了解到“已经选出一个Proposer Leader”这个事实,但是并不能知晓这个Proposer Leader仍然是“工作正常的”。所以一旦Proposer Leader被选出,它就会向所有其它Proposer发出一个“Start Working”指令,明确通知其它Proposer节点,自己开始正常工作了,并且在后续的工作中定时更新自己在各个Proposer节点上的租约时间(本文不介绍租约协议,有兴趣的读者可以自行查找该协议的介绍,这里可以简单理解为心跳)。

2-4-2. Multi-Paxos:正式工作过程

为了说清楚Multi-Paxos对多个业务提案的赋值投票过程,我们举一个连续进行日志写入的业务场景,这里每一条日志都有一个唯一且全局递增的log id——实际上一条日志就对应了一个提案即一个Paxos instance,提案内容就是“针对log id为X的日志,完成日志内容的持久化记录”。

这里有读者会问,这个唯一的log id是怎么被确认的呢?这个问题在Multi-Paxos中就很简单了,因为整个系统中只有一个Proposer Leader能进行提案的投票申请和赋值操作,所以这个来自于外部客户端的log id完全可以由Proposer Leader赋予一个唯一的Paxos instance id,并作为内部存取顺序的依据。如果这个log id来源于分布式系统内部那么这个问题就更简单了,直接由Proposer Leader赋予一个唯一且递增的log id即可。而后者的场景占大多数——试想一下这个基于Multi-Paxos算法工作的系统本来就是一个分布式存储系统……

这里写图片描述

由于Proposer Leader对业务数据的一致性处理跳过了Basic-Paxos算的第一个Prapare阶段,所以对于一条日志Log id,Proposer Leader会直接携带log id(可能还有Paxos instance id)和日志内容,向所有节点发起提案赋值操作。每一个Acceptor在收到赋值请求后,会首先在本地持久化这个日志内容。如果持久化成功,则对这个提案的赋值结果为“完成”,其它任何情况都是“未完成”。

Multi-Paxos的正式工作可以保证一批要进行投票的操作,其序号一定由小到大排列(对于这个日志业务的实例,就是日志的log id一定全局唯一且单调递增)。而且由于只存在一个Proposer Leader能够处理发起投票和赋值操作,所以也避免了操作竞争的问题,这直接导致每一个Paxos instance id都省去了Basic-Paxos的第一阶段,减少了花费在网络上的耗时。但是Multi-Paxos并非没有缺点,最显著的一个问题就是,由于Multi-Paxos中只有一个Proposer在工作,那么当这个Proposer由于任何原因无法在继续工作时怎么办?Multi-Paxos的解决办法是从剩下的Proposer,再重新选择一个Proposer Leader,让整个Multi-Paxos系统继续工作。

话虽简单,但实现起来却并不容易,这也是Multi-Paxos算法的难点所在。为什么呢?我们首先应该思考一下当原有的Proposer Leader宕机时,整个系统的可能处在什么样的状态,如下图所示:

这里写图片描述
(宕机前夜)

  • 首先由于Paxos算法的多数派原理,所以除了当前Proposer Leader(后文称为old leader)所在Server节点上的Log数据是完整的以外,整个系统不能保证任何一个其它Proposer所在Server节点上的Log数据是完整的。既然Log数据不完整,那么怎么做数据恢复呢?

  • 分布式系统的一般要力争 7 × 24小时不间断工作,也就是说即使整个系统连续工作1周时间,积累的LOG数据也是非常庞大的,10GB、100GB、1TB、10TB甚至更庞大的日志数据规模都有可能。

  • 当old leader宕机时,唯一可以确认的一件事情是:当前通过Multi-Paxos第二阶段操作,并持久化存储在Proposer Leader上的每一条日志数据,在整个Paxos算法算法系统中,至少存在N/2 + 1的多数派份额。在Proposer Leader宕机后,多数派需要达到的数量变成了(N - 1) / 2 + 1.

  • 最后,如上图所示Proposer Leader正在对log id为X7的数据进行Multi-Paxos第二阶段操作,且还没有拿到多数派确认。那么这样的数据在另一个Proposer Leader拿到控制权后,是恢复呢还是不恢复呢?

所谓重新恢复一个Proposer Leader(后文件称new leader),要达到的目的可不是简单的重新选举一个就行了。而是要让这个new leader尽量恢复到old leader宕机时的工作状态,当Client查询第X条日志信息时,new leader能够给出正确的值,而不是向Client回复not found或者错误的值。所以要保证new leader能够接替old leader继续工作,在old leader存活期和new leader恢复阶段,它们至少要做以下工作:确认赋值、确保old leader真正下线、重选leader和恢复数据。

2-4-3. Multi-Paxos:确认赋值过程(confirm)

确认赋值过程的目的主要是帮助Paxos系统在恢复阶段尽可能少做无用功,其做法是按照一定规则周期性的批量的发送Proposer Leader上log id的confirm信息。

这里写图片描述

如上图所示,所谓log id的confirm信息是指满足这样条件的log id信息,这个log id通过进行Multi-Paxos的第二阶段操作后,已经得到了大多数Acceptor的赋值确认,并且已经在Proposer Leader完成了持久化存储的log id。confirm信息还可以是乱序,甚至还可以是跳跃的。而且Proposer Leader发送一批og id的confirm信息后,也无需等待Proposer都回发确认信息,甚至可以设定Proposer成本身就不回发确认信息。

Proposer在收到这些confirm信息后,会检查(会可以不检查)log id confirm信息所对应的log id是否存在,如果存在则将这个log id confirm信息进行持久化保存;如果不存在也可以进行log id confirm信息的持久化保存(只是那样做没有意义),所以才会有“可以不预先检查”的说明。为什么要这样做,我们在后续4-4-5介绍数据恢复的小节会进行说明。

2-4-4. Multi-Paxos:确认下线

请考虑这样一个问题:当分布式系统中A节点收不到B节点“工作正常”的信息时,是A节点下线了,还是B节点下线了?答案是根本说不清楚,可能B节点还在正常工作,A节点收不到信息只是由于暂时的网络抖动;由于分布式系统存在分区隔离的问题,虽然A节点收不到B节点“工作正常”的信息,但是整个系统中还可能有大部分节点能够收到这样的信息,所以都不会认为B节点下线了;最后,也可能B节点真的已经下线。

在分布式系统中,对于节点X(既是本文一直在讨论的Proposer Leader节点)是否下线了这个问题很明显不是一个节点能够决定的,而最好是由多数派节点投票确认。在节点X被认定下线的时候,整个分布式系统至少需要保证两件事:

  • 多数派认为节点X下线了:这个原因已经在上一段落进行了描述,这里就不再赘述了。

  • 原来的节点X真的下线了:这是因为如果原有的节点X没有下线,在新的节点X1接替它工作后,就可能出现两个节点都在完成相同的工作职责,而它们接受到的传入数据又可能不完全一致,最终影响数据的一致性。要解决这个问题,只需要在各个节点实现租约协议。当节点X无法向多数派节点续租时即认为自己下线,这时即使自己工作是正常的,也会强制自己停止服务。

这里写图片描述

如上图所示,当节点Server N上关于Old Proposer Leader的租约信息过期后,Server N会发起一轮新的议题,议题内容为“Old Proposer Leader已经下线”。分布式系统中的其它节点在收到这个议题后,会检查自己和Old Proposer Leader的租约是否过期,如果没有过期,则可以在Basic-Paxos算法的第一阶段拒绝授权或者在Basic-Paxos算法的第二阶段拒绝赋值

当节点Server N关于“Old Proposer Leader已经下线”的议题通过,确确实实知晓不是由于自身的原因误认为“Old Proposer Leader已经下线”后,才会开始重新选主过程。这里有一个细节,当Server N的议题“Old Proposer Leader已经下线”通过后,Old Proposer Leader也会真实离线。这是因为在Old Proposer Leader上和多数派节点的租约都已经过期,即使Old Proposer Leader工作是正常的,也会强制自己停止服务。

需要注意的是,确认下线这个过程实际上并不是Multi-Paxos算法所独有的,而是所有分布式系统需要具备的功能。确认下线这个过程也并不是一定要使用Basic-Paxos算法进行,其原则是保证多数派确认下线和Old Proposer Leader确实下线这个事实。由于Multi-Paxos算法的特点,在正式工作时只能有一个Proposer Leader,所以在进行Proposer Leader重选前就必须确保原有的Proposer Leader真实下线。

2-4-5. Multi-Paxos:重选和数据恢复

重新选主的过程实际上和2-4-1小节描述的选主过程是一致的,甚至为了简化工作过程也可以将重新选主的提议和确认下线的提议合并在一起。重选步骤的关键并不在于如何选主,而在于如何恢复数据——因为Multi-Paxos的工作过程,我们不能保证任何一个Server节点上拥有完整的数据,所以一个New Proposer Leader在开始工作前就需要对数据完整性进行检查,对缺失的数据进行恢复,这样才能保证客户端在查询第Y条日志数据时,New Proposer Leader会返回正确的查询结果,而不是返回Not Found!下图展示了一种New Proposer Leader被选出后,整个Paxos集群所处理的数据状态

这里写图片描述

好消息是在Old Proposer Leader工作阶段,已经向潜在的成为新主的多个Proposer节点写入了不少的信息。例如,在Old Proposer Leader正式工作开始之前,会向其它Proposer节点写入一个start work的位置,记录前者开始工作时所用的log id的开始编号位置,经历了多少次Proposer Leader选主确认过程,就会有多个start work位置。例如一个月前由于Old Proposer Leader宕机,进行了一次选主,上周5由于Old Proposer Leader宕机又进行了一次选主。期间工作的各个Proposer上就会有两个start work位置。新加入的节点和恢复正常工作的历史节点,也应在加入/重新加入分布式环境时同步这些start work位置。

在Proposer Leader工作工程中,直接使用Basic-Paxos算法的第二阶段进行log id和内容的赋值操作,所以每一条操作成功的log内容都会记录在多个Proposer节点上,且这些节点的数量是多数派。最后,Proposer Leader工作过程中还会批量发送confirm信息,告知所有Proposer节点,哪些log id的内容已经得到了多数派确认。

所以在New Proposer Leader被选出,且开始进行数据恢复前。这个New Proposer Leader至少知道三类信息:一个或者多个Old Proposer Leader的strat working的位置、目前还在线的节点以及在Old Proposer Leader上已经得到多数派确认的若干log id信息。但是New Proposer Leader并不知道全局最大的log id,虽然在New Proposer Leader节点上会有一个max log id,但它并不知道后者是不是就是全局最大的log id。原因是之前Old Proposer Leader下线时,正在等待多数派确认的那个log id还没有得到多数派赋值确认又或者虽然多数派确认了但Old Proposer Leader本身还没有拿到响应信息,而New Proposer Leader自己并不清楚自己是否属于多数派。

数据恢复的原理说起来很简单,既是从数据恢复的开始点,到数据恢复的结束点,将本节点缺失的数据补全。那么问题来了:数据恢复的起点在哪?数据恢复的结束点在哪?哪些数据需要补全?数据来源在哪里?又应该如何进行补全?

  • 数据恢复的开始点很好确定,即是还没有开始恢复的第一个start work位置所对应的log id。

  • 数据恢复的结束点稍微麻烦一点,需要发起一轮Paxos投票来确认由多数派认可的那么全局最大的log id。不过,由于这时整个系统中已经确认了一个唯一的Proposer Leader,所以不存在投票冲突的问题,可以直接进行Basic-Poxos的第二阶段。

  • 从数据恢复的起点到终点,每一条log数据都需要进行一次完整的Basic-Poxos操作用以确保恢复的数据是历史上以形成多数派的正确数据,最终目的是为了保证数据最终一致性。但是之前已存在于New Proposer Leader上,且被标记为confirm的log数据则无需进行这样的操作了,因为Old c已经在之前确定了这些数据是正确的。这样就大量减少了需要恢复的数据规模,加快了数据恢复过程。

  • 注意,如果在数据恢复过程中,某一条Log id无法形成多数派决议,则这条数据就是在Old Proposer Leader中还没有被确认的数据,这样的数据不能进行恢复。即使在New Proposer Leader上有这条记录,如果没有形成多数派决议也不能使用,否则就会出现一些的错误。请看如下所示的极端例子:

==================================
(接后文)

作者:yinwenjie 发表于2017/3/20 18:34:29 原文链接
阅读:181 评论:0 查看评论

Android逆向之旅---获取加固后应用App的所有方法信息

$
0
0

一、前言

在逆向应用的时候,我们有时候希望能够快速定位到应用的关键方法,在之前我已经详细介绍了一个自己研发的代码动态注入工具icodetools,来进行动态注入日志信息到应用中,不了解的同学可以查看这里:icodetools动态注入代码解析,看完之后都会发现这个工具对于现在加固的应用是没有办法的,所以我们如何能够得到加固的应用的所有方法信息呢?再不用复杂的脱壳步骤也可以轻松的获取对应的方法信息。这个就是本文需要介绍的内容。


二、获取加固应用方法

在之前了解过加固应用原理的同学或者是弄过脱壳的同学都知道,加固其实就是对源apk进行加密,然后进行解密加载运行,所以在这个过程中如果我们想要得到应用的所有方法信息,必须是在解密加载运行之后进行操作了。这个我们就需要借助系统的一个重要类DexFile,下面可以查看他的源码:


看到这三个方法,而这三个方法是一个应用中类加载运行的最终点,不管你怎么加固,最终都会需要解密,加载dex,加载类然后运行。所以这三个地方是应用运行的必经之地。看到三个方法的参数信息,可以知道这个类的名称和类加载器,那么我们就可以利用这两个信息通过反射获取这个类的所有方法信息,这里依然需要借助神器Xposed来进行hook操作,hook的对象就是系统的dalvik.system.DexFile类的三个方法:


注意:为了防止多dex进行hook发生类找不到的错误,这里做一次attach操作!这里为了演示加固应用,就选择了之前我写过的一个下载今日头条视频的小工具,当时没有开源,并且进行了加固,选择的是梆梆加固。也有人找我要源代码。那么通过本文案例分析之后,就不需要源码了,你可以窥探这个小工具的所有方法信息了。这个工具的下载地址:https://pan.baidu.com/s/1eR56t94,使用教程在应用中也有。或者不了解的可以看这篇文章:Python脚本下载今日头条视频


下面继续来看看上面hook之后的处理类方法:


在这个方法中,首先我们通过类名和加载器信息,得到这个类所有的方法信息,然后开始构造这个方法的签名信息:


这个信息包括:方法的修饰符,返回类型,方法名,参数类型,大致最终的样式为:

public final native java.lang.String xxx(java.lang.String, int)

这样的信息就能够把一个方法的所有信息体现出来,后续如果我们想在外部利用反射调用这个方法来窥探信息就好办了,得到这些方法的签名信息之后,然后就开始进行每个方法的hook操作了:


在利用Xposed进行hook一个方法的时候,需要知道类名,类加载器,方法名,方法参数类型。正好这些信息在上面都可以知道,而这里之所以要进行hook是为了能够判断这个方法是否被调用了,方法在被调用的时候的参数值是什么。因为上面获取是这个类所有的方法,但是不代表这些方法就是被调用了。只有在次进行hook才可以判断当前方法被调用了。而这里有一个问题就是如果一个应用过于庞大,方法较多。在Xposed进行hook那么多方法就会出现OOM这样的错误。所以本文为了演示就没有做优化,我们可以先获取类的所有方法,然后分批进行hook操作即可。


三、运行效果

下面我们运行这个Xposed模块,然后重启设备运行即可,最后在运行上面已经安装加固的今日头条下载器工具,运行看效果日志:


看到了,我们把这个工具的所有方法都打印出来了,然后再来看看他的调用方法:


从这些信息可以看到,这些方法被实际调用了,再来看看那些方法被调用时的参数值:


看到了,方法调用的参数值我们也可以把它打印出来。


四、如何处理获取到的方法

到这里我们达到了我们的目的,可以dump出一个加固应用的所有方法,以及被实际调用方法,同时还可以把参数值打印处理,本文用的是梆梆加固的案例,其实其他家的加固都是可以的。感兴趣的同学可以尝试一下。下面继续来看,上面获取到了应用的所有方法信息,有的同学可以会想这么干,用反射调用某个可疑方法,看看方法的返回值是啥?所谓可以,可能需要通过经验,结合方法的参数信息类型得知这个方法的可疑程度,反射调用很简单:


可以自己随便写一个应用然后得到根据包名得到其对应的Context变量,然后得到其类加载器,开始加载这个可以方法,不过可惜的是,这个运行应该是报错的:


提示找不到这个类,原因想一下就通了:因为现在的应用是加固的,加固最终解密dex运行肯定用的是自己的类加载器,而上面的这种方式获取到的Context只是外层壳的类加载器也就是应用默认的PathClassLoader加载器,所以理论上肯定是找不到这个类的,所以如果想不报错,就需要找到这个类加载器。但是这个因为没有源码所以很难找到。所以通过这个简单的代码想告诉一些同学,这种想法是好的,但是其实是错误的。


取而代之的是我们可以利用Xposed来进行hook这个方法,因为Xposed中我们可以解决类加载器的问题:


先hook系统的Application类的attach方法,然后在获取他的Context值,得到类加载器,来加载指定类,如果加载成功了,所以找到了对应的类加载器,那么就可以进行后续的hook操作了。这种方法和思想是对的。


五、解答疑惑

有的同学又好奇了,上面我们在进行获取类的所有方法信息的同时不是已经进行hook方法了,还得到方法运行时的参数值了,的确那里是做了,但是我也说了,Xposed一次性hook太多方法是会报错的,因为本文案例的应用方法数不是很多,所以看起来很正常,如果进行hook大的应用的时候会出现问题的。所以为了解决这样的问题,我们可以先获取应用的所有方法,此时可以不进行hook操作,然后有了类的所有方法信息之后,在进行方法分析,然后对可以方法单独进行hook操作来进行验证即可。


有的同学又有疑问了?这个对于大型的app来说感觉没什么用?的确,比如像WX这样的应用,如果用作本文案例,可以看到日志刷屏了,没有一会就出现ANR了,这个问题在之前说到用icodetools工具进行操作的时候也出现过。当时的解决方案是通过开关进行控制,这里其实也可以用开关进行控制的。但是这里想说的是,现在加固的应用并不是大型应用,原因很简单,因为加固是有崩溃率的,现在没有任何一个加固平台可以保证零崩溃率的,因为加固现在都涉及到了native层,而Android机型复杂,兼容性肯定不好。所以对于那些用户量非常庞大的应用是不会进行加固的,比如BAT的主流产品都没有进行加固。但是有的应用必须进行加固,那就是涉及到金融方面的,对于他们来说安全比崩溃率更重要。也有的大部分收费和内购的游戏也是选择加固的。而这种应用现在大部分属于中型应用,本文还是可以进行操作的。


项目下载地址:https://github.com/fourbrother/XposedHookAllMethod


六、总结

本文主要讲解了如何hook系统类信息,然后获取到加固应用之后的所有方法信息,同时还可以获取到哪些方法被执行了,以及执行的过程中参数信息,而这些信息是分析一个应用最为重要的信息。当然我们也可以通过分析方法的参数信息来判断这个方法的可以程度,然后在用Xposed进一步进行hook来验证结果都是可以的。介绍了本文的内容,如果大家觉得文章有帮助的同学可以多多点赞和分享扩散,要是有打赏那就最好了!


更多内容:点击这里

关注微信公众号,最新技术干货实时推送


编码美丽技术圈
微信扫一扫进入我的"技术圈"世界


扫一扫加小编微信
添加时请注明:“编码美丽”非常感谢!


作者:jiangwei0910410003 发表于2017/3/20 18:42:40 原文链接
阅读:1126 评论:1 查看评论

CSDN日报20170320——《Java 程序员的面试经历和题库》

$
0
0

【程序人生】Java 程序员的面试经历和题库

作者:nuaazhaofeng

最近打算换城市了,受不了北京的雾霾了,所以准备逃离啦。所以一直在面试中,整理了下最近遇到的一些面试题,供大家参考。其中会包含一些面试的小经验,如果您是面霸,希望能给予指导。自己不是大牛,如果您是大牛,也可以忽略之。我面试的岗位是Java后端开发工程师。


【Python】7行Python代码的人脸识别

作者:半吊子全栈工匠

随着去年alphago 的震撼表现,AI 再次成为科技公司的宠儿。AI涉及的领域众多,图像识别中的人脸识别是其中一个有趣的分支。百度的BFR,Face++的开放平台,汉王,讯飞等等都提供了人脸识别的API,对于老码农而言,自己写一小段代码,来看看一张图片中有几个人,没有高大上,只是觉得好玩,而且只需要7行代码。


【Android 开发】原生 Android 也能做 Web 开发了

作者:严振杰

AndServer 是一个 Android 端的 Web 服务器,类似 Apache 或者 Tomcat ,但又有不同,它是一个普通的 Android Library, Android 项目 Gradle 远程依赖或者添加 Jar 包皆可引入该项目,然后就通过正常 Android 开发一样开发 App 了。

AndServer 是用纯 Android API 写一个库,所以不用任何第三方的库或者什么硬件编译,打成 Jar 包后仅仅 580kb。


【深度学习】深度学习之图像修复

作者:张雨石

图像修复问题就是还原图像中缺失的部分。基于图像中已有信息,去还原图像中的缺失部分。

从直观上看,这个问题能否解决是看情况的,还原的关键在于剩余信息的使用,剩余信息中如果存在有缺失部分信息的patch,那么剩下的问题就是从剩余信息中判断缺失部分与哪一部分相似。而这,就是现在比较流行的PatchMatch的基本思想。


【图形处理】基于物理的渲染技术(PBR)系列二

作者:姜雪伟

在这里我们引入了一种被称为渲染方程(Render Equation)的东西。它是某些聪明绝顶人所构想出来的一个精妙的方程式,是如今我们所拥有的用来模拟光的视觉效果最好的模型。基于物理的渲染所坚定的遵循的是一种被称为反射率方程(The Reflectance Equation)的渲染方程的特化版本。要正确的理解PBR 很重要的一点就是要首先透彻的理解反射率方程。


【Web 前端】Web 前端入坑第四篇:你还在用 jQuery?

作者:郭小北

jQuery过时了吗

这是个伪命题,但却是个很亲民和讨论意义的话题。

过时:如果是指被用得少了,或者大部分人已经在讨论是不是过时了,那肯定是在过时。


【系统运维】OSI/RM模型的编址方案与TCP/IP编址方案的对比

作者:赵亚

还是抛出老问题,IP地址到底是属于主机的还是属于网卡的?理解了这个看似无聊的问题,可以帮助你增强各种动态路由协议,IPv6等技术的理解深度。


【好书推荐】简单读懂人工智能:机器学习与深度学习是什么关系

作者:博文视点

随着AlphaGo战胜李世石,人工智能和深度学习这些概念已经成为一个非常火的话题。人工智能、机器学习与深度学习这几个关键词时常出现在媒体新闻中,并错误地被认为是等同的概念。本文将介绍人工智能、机器学习以及深度学习的概念,并着重解析它们之间的关系。本文将从不同领域需要解决的问题入手,依次介绍这些领域的基本概念以及解决领域内问题的主要思路。

本文选自《Tensorflow:实战Google深度学习框架》。


关注专栏【CSDN 日报】,获取最新及往期内容。

作者:blogdevteam 发表于2017/3/20 18:49:34 原文链接
阅读:2790 评论:3 查看评论
Viewing all 35570 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>