flybread 2016-12-02T03:31:51+00:00 kejinlu@gmail.com redis集群的应用 2016-11-30T00:00:00+00:00 flybread http://flybread.github.io//2016/11/1

1, Redis Sharding集群原理,使用的条件,优劣点以及对应的解决方案
2, Redis官方集群方案 Redis Cluster介绍
3,中间件的方式实现Redis集群
4,选择

redis 集群的解决的方案和原理分析

1, Redis Sharding集群

Redis Sharding采用客户端Sharding方式,服务端Redis还是一个个相对独立的Redis实例节点,没有做任何变动。理解就是客户端的连接的特定的服务器,但是在增加节点或者删除节点的时候,有一定的兼容的算法,使得原来打在某一个redis实例,还打在这个实例上面。

主要的思想就是根据一定的算法,例如 哈希算法,对Redis的key值进行散列,然后将key值散列到特定Redis实例上面。以后的设置,获取等关于该key值的操作,都会打到这个特定的redis实例上面进行操作。

值得庆幸的是,java redis的客户端驱动jedis,已经支持 Redis Sharding 功能。即是ShardedJedis以及结合缓存池ShardedJedisPool。

Jedis的Redis Sharding实现具有如下特点:

1.采用一致性哈希算法(consistent hashing),将key和节点name同时hashing,然后进行映射匹配,采用的算法是MURMUR_HASH。采用一致性哈希而不是采用简单类似哈希求模映射的主要原因是当增加或减少节点时,不会产生由于重新匹配造成的rehashing。一致性哈希只影响相邻节点key分配,影响量小。
即是:ShardedJedis 在操作的时候,都会调用的getShard方法,来获得key对应的redis实例:

  public R getShard(String key) {
    return resources.get(getShardInfo(key));
  }
  public S getShardInfo(String key) {
    return getShardInfo(SafeEncoder.encode(getKeyTag(key)));
  }
  public String getKeyTag(String key) {
    // 默认的情况下,tagPattern是NULL
    if (tagPattern != null) {
      Matcher m = tagPattern.matcher(key);
      if (m.find()) return m.group(1);
    }
    return key;
  }
  public static byte[] encode(final String str) {
    return str.getBytes(Protocol.CHARSET);
  }
  public S getShardInfo(byte[] key) {
// 最关键的treeMap的数据结构
    SortedMap<Long, S> tail = nodes.tailMap(algo.hash(key));
    if (tail.isEmpty()) {
      return nodes.get(nodes.firstKey());
    }
    return tail.get(tail.firstKey());
  }

  //nodes 是ShardedJedisPool 初始化的时候,hash Redis的实例的配置信息后组成的TreeMap,声明是:   

  private TreeMap<Long, S> nodes;//S extends ShardInfo<R>

2.为了避免一致性哈希只影响相邻节点造成节点分配压力,ShardedJedis会对每个Redis节点根据名字(没有,Jedis会赋予缺省名字)会虚拟化出160个虚拟节点进行散列。根据权重weight,也可虚拟化出160倍数的虚拟节点。用虚拟节点做映射匹配,可以在增加或减少Redis节点时,key在各Redis节点移动再分配更均匀,而不是只有相邻节点受影响。

private void initialize(List<S> shards) {
    nodes = new TreeMap<Long, S>();
    for (int i = 0; i != shards.size(); ++i) {
      final S shardInfo = shards.get(i);
      if (shardInfo.getName() == null)
        for (int n = 0; n < 160 * shardInfo.getWeight(); n++) {
          nodes.put(this.algo.hash("SHARD-" + i + "-NODE-" + n), shardInfo);
        }
      else
        for (int n = 0; n < 160 * shardInfo.getWeight(); n++) {
          nodes.put(this.algo.hash(shardInfo.getName() + "*" + shardInfo.getWeight() + n),
              shardInfo);
        }
      resources.put(shardInfo, shardInfo.createResource());
    }
  }

3.ShardedJedis支持keyTagPattern模式,即抽取key的一部分keyTag做sharding,这样通过合理命名key,可以将一组相关联的key放入同一个Redis节点,这在避免跨节点访问相关数据时很重要。(默认的情况下为NULL)

KeyTag这个可以在初始化ShardedJedisPool 设置。

public ShardedJedisPool(final GenericObjectPoolConfig poolConfig,
      List<JedisShardInfo> shards, Pattern keyTagPattern) {
        this(poolConfig, shards, Hashing.MURMUR_HASH, keyTagPattern);
  }

当然,Redis Sharding这种轻量灵活方式必然在集群其它能力方面做出妥协,但是针对每一种不好的地方,我们又都准备了不同的解决方案。

1 扩容,当想要增加Redis节点时,尽管采用一致性哈希,毕竟还是会有key匹配不到而丢失,这时需要键值迁移

这就要求:应用层面允许Redis中数据丢失或从后端数据库重新加载数据。但有些时候,击穿缓存层,直接访问数据库层,会对系统访问造成很大压力。

2 轻量级客户端sharding,处理Redis键值迁移比较的麻烦。

Redis作者给出了一个比较讨巧的办法–presharding,即预先根据系统规模尽量部署好多个Redis实例,这些实例占用系统资源很小,一台物理机可部署多个,让他们都参与sharding,当需要扩容时,选中一个实例作为主节点,新加入的Redis节点作为从节点进行数据复制。数据同步后,修改sharding配置,让指向原实例的Shard指向新机器上扩容后的Redis节点,同时调整新Redis节点为主节点,原实例可不再使用。

3 应从redis宕机的风险

为不影响Redis性能,尽量不开启AOF和RDB文件保存功能,可采用Redis主备模式,主Redis宕机,数据不会丢失,因为备Redis留有备份。这样,架构模式变成一个Redis节点切片包含一个主Redis和一个备Redis。在主Redis宕机时,备Redis接管过来,上升为主Redis,继续提供服务。主备共同组成一个Redis节点,通过自动故障转移,保证了节点的高可用性。

可以利用主从模式实现读写分离,主负责写,从负责只读,同时一主挂多个从。在Sentinel监控下,还可以保障节点故障的自动监测。

2, Redis官方集群方案 Redis Cluster

Redis Cluster是一种服务器Sharding技术,3.0版本开始正式提供。

Redis Cluster中,Sharding采用slot(槽)的概念,一共分成16384个槽。对于每个进入Redis的键值对,根据key进行散列,分配到这16384个slot中的某一个中。使用的hash算法也比较简单,就是CRC16后16384取模。

Redis集群中的每个node(节点)负责分摊这16384个slot中的一部分,也就是说,每个slot都对应一个node负责处理。 这个和我们搭建的集群的日志相吻合:

Redis集群

当动态添加或减少node节点时,需要将16384个槽做个再分配,槽中的键值也要迁移。当然,这一过程,在目前实现中,还处于半自动状态,需要人工介入。Redis集群,要保证16384个槽对应的node都正常工作,如果某个node发生故障,那它负责的slots也就失效,整个集群将不能工作。

为了增加集群的可访问性,官方推荐的方案是将node配置成主从结构,即一个master主节点,挂n个slave从节点。这时,如果主节点失效,Redis Cluster会根据选举算法从slave节点中选择一个上升为主节点,整个集群继续对外提供服务。这非常类似前篇文章提到的Redis Sharding场景下服务器节点通过Sentinel监控架构成主从结构,只是Redis Cluster本身提供了故障转移容错的能力。

Redis Cluster的新节点识别能力、故障判断及故障转移能力是通过集群中的每个node都在和其它nodes进行通信,这被称为集群总线(cluster bus)。它们使用特殊的端口号,即对外服务端口号加10000。例如如果某个node的端口号是6379,那么它与其它nodes通信的端口号是16379。nodes之间的通信采用特殊的二进制协议。

对客户端来说,整个cluster被看做是一个整体,客户端可以连接任意一个node进行操作,就像操作单一Redis实例一样,当客户端操作的key没有分配到该node上时,Redis会返回转向指令,指向正确的node,这有点儿像浏览器页面的302 redirect跳转。

3.利用代理中间件实现大规模Redis集群

上面分别介绍了多Redis服务器集群的两种方式,它们是基于客户端sharding的Redis Sharding和基于服务端sharding的Redis Cluster。

客户端sharding技术其优势在于服务端的Redis实例彼此独立,相互无关联,每个Redis实例像单服务器一样运行,非常容易线性扩展,系统的灵活性很强。其不足之处在于:

由于sharding处理放到客户端,规模进步扩大时给运维带来挑战。 服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整。 连接不能共享,当应用规模增大时,资源浪费制约优化。

服务端sharding的Redis Cluster其优势在于服务端Redis集群拓扑结构变化时,客户端不需要感知,客户端像使用单Redis服务器一样使用Redis集群,运维管理也比较方便。

不过Redis Cluster正式版推出时间不长,系统稳定性、性能等都需要时间检验,尤其在大规模使用场合。

能不能结合二者优势?即能使服务端各实例彼此独立,支持线性可伸缩,同时sharding又能集中处理,方便统一管理: Redis代理中间件twemproxy就是这样一种利用中间件做sharding的技术。

twemproxy处于客户端和服务器的中间,将客户端发来的请求,进行一定的处理后(如sharding),再转发给后端真正的Redis服务器。也就是说,客户端不直接访问Redis服务器,而是通过twemproxy代理中间件间接访问。

twemproxy中间件的内部处理是无状态的,它本身可以很轻松地集群,这样可避免单点压力或故障。

twemproxy又叫nutcracker,起源于twitter系统中redis/memcached集群开发实践,运行效果良好,后代码奉献给开源社区。其轻量高效,采用C语言开发,工程网址是:GitHub - twitter/twemproxy: A fast, light-weight proxy for memcached and redis

twemproxy后端不仅支持redis,同时也支持memcached,这是twitter系统具体环境造成的。

由于使用了中间件,twemproxy可以通过共享与后端系统的连接,降低客户端直接连接后端服务器的连接数量。同时,它也提供sharding功能,支持后端服务器集群水平扩展。统一运维管理也带来了方便。

当然,也是由于使用了中间件代理,相比客户端直连服务器方式,性能上会有所损耗,实测结果大约降低了20%左右。

那么,上面几种方案如何选择?

显然在客户端做分片是自定义能力最高的,优势在于,在不需要客户端服务端协作,以及没有中间层的条件下,每个请求的 roundtrip 时间是相对更小的,搭配良好的客户端分片策略,可以让整个集群获得很好的扩展性。当然劣势也很明显,用户需要自己对付 Redis 节点宕机的情况,需要采用更复杂的策略来做 replica,以及需要保证每个客户端看到的集群“视图”是一致的。

中间件的方案对客户端实现的要求是最低的,客户端只要支持基本的 Redis 通信协议即可,至于扩容、多副本、主从切换等机制客户端都不必操心,因此这种方案也很适合用来做“缓存服务”。

官方推出的协作方案也完整地支持了分片和多副本,相对于各种 proxy,这种方案假设了客户端实现是可以与服务端“协作”的,事实上主流语言的 SDK 都已经支持了。

]]>
杂记 2016-10-25T00:00:00+00:00 flybread http://flybread.github.io//2016/10/3

记录自己的一些的问题:

  1. git冲突的编写
  2. 自己应该做什么?

git冲突后

当两个改动发生在同一个文件的同一些行上,我们就要看看发生冲突的文件的内容了。Git 会非常友好地把文件中那些有问题的区域在 “«««< HEAD” 和 “»»»> [other/branch/name]” 之间标记出来

git冲突

第一个标记后的内容源于当前分支。在尖括号之后,Git 会告诉我们这些改动是从哪里(哪个分支)来的。然后有冲突的改动会被 “=======” 分割起来。

MAC OSX 的开发

1,做出一个比较像样子的zk的配置工具,把

]]>
回溯算法 2016-10-24T00:00:00+00:00 flybread http://flybread.github.io//2016/10/2

回溯算法的描述

回溯算法

Backtracking is a general algorithm for finding all (or some) solutions to some computational problems, notably constraint satisfaction problems, that incrementally builds candidates to the solutions, and abandons each partial candidate c (“backtracks”) as soon as it determines that c cannot possibly be completed to a valid solution.[1][2] 针对一般的计算问题,特别是满足一定约束条件的问题,回溯算法是一个比较常见的解决方式,即是逐步的构建针对解决方案的候选方案,一旦确定候选方案不满足约束条件,不是一个解决方案时候立即的放弃。

The classic textbook example of the use of backtracking is the eight queens puzzle, that asks for all arrangements of eight chess queens on a standard chessboard so that no queen attacks any other. In the common backtracking approach, the partial candidates are arrangements of k queens in the first k rows of the board, all in different rows and columns. Any partial solution that contains two mutually attacking queens can be abandoned. 经典的回溯算法的案例就是八皇后问题,八皇后问题是在一个棋盘上面放置八个不能够互相攻击其他皇后的皇后,在经典的回溯算法中,候选的方案是K个皇后,棋盘的K行,包括皇后的位置处于所有的行和所有的列,任何一个两个皇后能够互相攻击的方案直接的舍弃。

Backtracking can be applied only for problems which admit the concept of a “partial candidate solution” and a relatively quick test of whether it can possibly be completed to a valid solution. It is useless, for example, for locating a given value in an unordered table. When it is applicable, however, backtracking is often much faster than brute force enumeration of all complete candidates, since it can eliminate a large number of candidates with a single test. 回溯算法适合拥有部分候选方案的问题,并且问题能够在相对比较短的时间内验证候选方案。对于无需表中确定一个确定值的位置这样的,回溯算法是不适合的。然而,相比较暴力破解的算法,回溯算法因为能够在一次的测试中放弃大量的候选的方案,所以还是比较快的。

Backtracking is an important tool for solving constraint satisfaction problems, such as crosswords, verbal arithmetic, Sudoku, and many other puzzles. It is often the most convenient (if not the most efficient[citation needed]) technique for parsing, for the knapsack problem and other combinatorial optimization problems. It is also the basis of the so-called logic programming languages such as Icon, Planner and Prolog.

对于满足约束条件的问题,例如填字游戏,语言运算,数独等类似的猜谜的问题,回溯是一个比较重要的工具。另外对于背包问题,组合优化,语法分析也是一个比较方便的工具。另外还是一些逻辑编程语言Icon, Planner and Prolog的基础

Backtracking depends on user-given “black box procedures” that define the problem to be solved, the nature of the partial candidates, and how they are extended into complete candidates. It is therefore a metaheuristic rather than a specific algorithm – although, unlike many other meta-heuristics, it is guaranteed to find all solutions to a finite problem in a bounded amount of time. 回溯取决于用户给定的“黑箱程序”,其定义了要解决的问题,部分候选的性质以及如何将其扩展为完整候选。因此,它是元启发式而不是特定的算法 - 尽管与许多其他元启发式算法不同,它保证在有限时间内找到有限问题的所有解。

Description of the method[]

The backtracking algorithm enumerates a set of partial candidates that, in principle, could be completed in various ways to give all the possible solutions to the given problem. The completion is done incrementally, by a sequence of candidate extension steps. 回溯算法枚举一组部分候选,原则上可以以各种方式给出给定问题的所有可能的解决方案。然后通过选定的步骤逐步地解决。

Conceptually, the partial candidates are represented as the nodes of a tree structure, the potential search tree. Each partial candidate is the parent of the candidates that differ from it by a single extension step; the leaves of the tree are the partial candidates that cannot be extended any further. 在概念上,部分候选方案被表示为树结构的节点,即潜在搜索树。每个部分候选是通过单个扩展步骤与其不同的候选的父代;树的叶是不能进一步扩展的部分候选方案。

The backtracking algorithm traverses this search tree recursively, from the root down, in depth-first order. At each node c, the algorithm checks whether c can be completed to a valid solution. If it cannot, the whole sub-tree rooted at c is skipped (pruned). Otherwise, the algorithm (1) checks whether c itself is a valid solution, and if so reports it to the user; and (2) recursively enumerates all sub-trees of c. The two tests and the children of each node are defined by user-given procedures. 回溯算法从根向下以深度优先顺序递归地遍历该搜索树。在每个节点c,算法检查c是否可以达到有效解条件。如果不能,则跳过(修剪)以c为根的整个子树。否则,算法(1)检查c本身是否是有效解,并且如果是,则将其报告给用户;和(2)递归地枚举c的所有子树。节点和每个节点的子节点的测试都是由用户给定的程序来校验。

Therefore, the actual search tree that is traversed by the algorithm is only a part of the potential tree. The total cost of the algorithm is the number of nodes of the actual tree times the cost of obtaining and processing each node. This fact should be considered when choosing the potential search tree and implementing the pruning test.

因此,回溯遍历的是潜在搜索树的一部分,但是总体的算法的花费是树的全部的节点乘以每一个节点的检查时间。这一点应该在实现算法的时候加以考虑。

Pseudocode

In order to apply backtracking to a specific class of problems, one must provide the data P for the particular instance of the problem that is to be solved, and six procedural parameters, root, reject, accept, first, next, and output. These procedures should take the instance data P as a parameter and should do the following:

  • [ ] root(P): return the partial candidate at the root of the search tree.
  • [ ] reject(P,c): return true only if the partial candidate c is not worth completing.
  • [ ] accept(P,c): return true if c is a solution of P, and false otherwise.
  • [ ] first(P,c): generate the first extension of candidate c.
  • [ ] next(P,s): generate the next alternative extension of a candidate, after the extension s.
  • [ ] output(P,c): use the solution c of P, as appropriate to the application.

The backtracking algorithm reduces the problem to the call bt(root(P)), where bt is the following recursive procedure:

procedure bt(c) if reject(P,c) then return if accept(P,c) then output(P,c) s ← first(P,c) while s ≠ Λ do bt(s) s ← next(P,s)

Usage considerations

The reject procedure should be a boolean-valued function that returns true only if it is certain that no possible extension of c is a valid solution for P. If the procedure cannot reach a definite conclusion, it should return false. An incorrect true result may cause the bt procedure to miss some valid solutions. The procedure may assume that reject(P,t) returned false for every ancestor t of c in the search tree. reject是一个返回boolean值的方法,只有当c不可能是p的一个解的时候,才会返回true。如果这个方法不能够确定结论,就返回false。如果方法在不正确的情况,返回了true,就可能会错过某些正确的解。这个方法reject(p,t)搜索树t的根节点时候,可以假设返回false。

On the other hand, the efficiency of the backtracking algorithm depends on reject returning true for candidates that are as close to the root as possible. If reject always returns false, the algorithm will still find all solutions, but it will be equivalent to a brute-force search. 另外一方面,回溯算法的效率和reject返回true的时候,节点离根点的远近成比例。如果reject总是返回false,那么此时的回溯就相当于暴力算法。 The accept procedure should return true if c is a complete and valid solution for the problem instance P, and false otherwise. It may assume that the partial candidate c and all its ancestors in the tree have passed the reject test. 如果c是问题实例P的完整有效解,则accept过程应返回true,否则返回false。它可以假设树中的部分候选c及其所有祖先通过了拒绝测试 Note that the general pseudo-code above does not assume that the valid solutions are always leaves of the potential search tree. In other words, it admits the possibility that a valid solution for P can be further extended to yield other valid solutions. 注意,上面的一般伪代码不假设有效解决方案总是潜在搜索树的叶。换句话说,它承认P的有效解可以进一步扩展以产生其他有效解的可能性。

The first and next procedures are used by the backtracking algorithm to enumerate the children of a node c of the tree, that is, the candidates that differ from c by a single extension step. The call first(P,c) should yield the first child of c, in some order; and the call next(P,s) should return the next sibling of node s, in that order. Both functions should return a distinctive “null” candidate, denoted here by ‘Λ’, if the requested child does not exist.

回溯算法使用first 和 next方法 来遍历树的节点c的所有的子节点,即使候选方案通过c进一步的扩展。first(P,c) 应该是c的第一个孩子,按照某种的顺序,next(P,s)应该是s的兄弟节点,两个函数都有返回 null 的情况,这个使用’Λ’ 代表子节点不存在的情况。

Together, the root, first, and next functions define the set of partial candidates and the potential search tree. They should be chosen so that every solution of P occurs somewhere in the tree, and no partial candidate occurs more than once. Moreover, they should admit an efficient and effective reject predicate.

root first next 这些方法合起来标识潜在搜索树的候选方案的集合,这个集合中包含了树种全部的候选方案,并且候选方案没有重复。另外这些候选方案可以被 reject 方法处理。

Early stopping variants

The pseudo-code above will call output for all candidates that are a solution to the given instance P. The algorithm is easily modified to stop after finding the first solution, or a specified number of solutions; or after testing a specified number of partial candidates, or after spending a given amount of CPU time.

伪代码中是输出全部的解决的方案,如果修改为找到一个解决方案就返回,或者找个几个解决方案就返回,亦或是校验了特定数量的备选方案或者过了特定的时间 就返回,也不是什么难事。

]]>
梳理一些的基础性的知识 2016-10-23T00:00:00+00:00 flybread http://flybread.github.io//2016/10/01
  1. tomcat的体系结构
  2. 自己对tomcat感兴趣的地方
    2.1 tomcat的请求接入的过程
    2.2 servet的生命周期的加载
    2.3 tomcat是怎么部署的,具体是怎么处理的。多个应用之间是怎么相互隔离的?tomcat的classloader机制是怎样的?
    2.4 关闭服务的时候,应该先暂停/关闭什么,后关闭什么?
    2.5 tomcat的扩展机制是通过什么方式提供给开发者的?
    2.6 BIO/NIO/APR 三种connector实现上的优劣是什么?
    2.7 jsp文件是怎么编译和执行的?
  3. tomcat中设计到的设计模式

1. tomcat 的体系结构

首先就是tomcat的体系架构,在这个在很多的博客中都有提到,但是看他们的总没有自己看源码,看书中理解的透彻。

]]>
自我的剖析 2016-10-08T00:00:00+00:00 flybread http://flybread.github.io//2016/10/7

构建企业服务的过程中,我们采用的技术的分析,外加复盘的想法,理解自己项目中采用的技术,提高自己对架构的理解和对技术的眼界,首页我们要说的是tomcat

####企业服务架构复盘 #####1. 目的

我们的工程是一个典型的springMVC提供的企业应用开发服务,在不泄密的前提下,分析一下这些工程的使用的技术和应用的场景,也就是复盘一下,提高自己对技术的眼界。

2.具体的应用的技术

2.1 tomcat

服务运行的环境是tomcat,但是关于tomcat的运用的方面是一个tomcat服务,对应一个服务。

当然这个是针对分离服务的框架,把企业的服务按照逻辑的划分,例如打卡,审批,财务等等的大块的逻辑,分成了不同的服务,然后按照技术人员严重的逻辑的划分,按照比较大的粒度分装成服务,我们现在说的服务,指的就是这个服务。
这种方案的划分的好处:一,服务拆分了出来之后,能够互相的不影响。对代码的跟新,不同模块版本的发布,资源的分配(例如企业服务中的请假服务压力就不会很大,一般人员不会每天都请假,针对这种情况的服务我们可以分配少一点的CPU,内存等资源)等有很大的好处。但是也有一定不好的地方,例如工程的管理当面,运维的管理。还是体现出现了那句话:
粒度小便于控制但是不便于管理。

这个地方必须的说一个tomcat,web应用开发的学习者第一次接受的web的容器必然是tomcat,这个可以说是最常见的,也是应用比较广泛的servlet容器。但是真正的理解和深入的了解并不是很多。自己的项目在使用tomcat的时候,自己是否应该好好的学习一下,最起码自己项目中用到的部分应该清楚理解。
首先是此篇说明tomcat的依据是:how tomcat works。

这本书于2016年10月23日看完,对tomcat的体系有了进一步的了解,找时间写一个总结。

]]>
算法,基础类 2016-09-22T00:00:00+00:00 flybread http://flybread.github.io//2016/09/30

基本的算法的描述,起到一个目录的作用,然后我们看看自己还有什么样的算法没有实现,没有说明白,把一个个的算法的名称,变为一个个的链接地址。

算法描述:

1.排序算法

冒泡算法 插入排序

选择排序 堆排序

归并排序 快速排序

希尔排序 桶排序

计数排序 基数排序

2.栈和队列

队列

3.树的结构

]]>
队列算法描述+代码实现 2016-09-22T00:00:00+00:00 flybread http://flybread.github.io//2016/09/26

队列算法描述+代码实现

队列的接口


    // 入队列,如果空间不够则抛出异常
    boolean add(E e);

    // 和add的逻辑很相似
    boolean offer(E e);

    //出队列,没有元素的时候抛异常
    E remove();

    //出队列,没有元素的时候为null
    E poll();

    //取队头,队列为空时抛异常
    E element();

    //取队头,队列为空的时候为空
    E peek();

具体的实现的逻辑就是:

/**
 * @author zhailz
 *
 * 时间:2016年9月26日 ### 下午3:51:35
 *
 */
public class MyQueue<E> {

	/**
	 * 队列对外提供的方法,说简单也挺简单的,只有两个:出队列和入队列
	 * 就队列的实现可以分为两类:数组和链表。
	 * 就队列的支持来说也可以分支两类:线程安全的,和线程不安全的
	 * */

	transient Object[] elements; // non-private to simplify nested class access
	transient int head;
	transient int tail;
	private static final int MIN_INITIAL_CAPACITY = 8;

	public MyQueue() {
		elements = new Object[16];
	}

	public MyQueue(int numElements) {
		allocateElements(numElements);
	}

	public void add(E e) {
		if (e == null)
			throw new NullPointerException();
		elements[tail] = e;
		/**
		 * tail = (tail + 1) 尾巴上面增加1
		 * 先下的值对数组的长度取余 与 head的值进行比较,确定队列时候已满
		 * tail & (elements.length - 1) == head
		 *
		 * */
		if ((tail = (tail + 1) & (elements.length - 1)) == head)
			doubleCapacity();
	}

	public E remove() {
		E result = pollFirst();
		if (result == null)
			throw new NoSuchElementException();
		return result;
	}

	public E poll() {
		E result = pollFirst();
		return result;
	}

	public E pollFirst() {
		int h = head;
		@SuppressWarnings("unchecked")
		E result = (E) elements[h];
		// Element is null if deque empty
		if (result == null)
			return null;
		elements[h] = null; // Must null out slot
		//出队列的时候的,对头的增长
		head = (h + 1) & (elements.length - 1);
		return result;
	}

	public E element() {
		return getFirst();
	}

	public E getFirst() {
		@SuppressWarnings("unchecked")
		E result = (E) elements[head];
		if (result == null)
			throw new NoSuchElementException();
		return result;
	}

	@SuppressWarnings("unchecked")
	public E peek() {
		// elements[head] is null if deque empty
		return (E) elements[head];
	}

	private void doubleCapacity() {
		assert head == tail;
		int p = head;
		int n = elements.length;
		int r = n - p; // number of elements to the right of p
		int newCapacity = n << 1;
		if (newCapacity < 0)
			throw new IllegalStateException("Sorry, deque too big");
		Object[] a = new Object[newCapacity];
		System.arraycopy(elements, p, a, 0, r);
		System.arraycopy(elements, 0, a, r, p);
		elements = a;
		head = 0;
		tail = n;
	}

	//分配元素很有趣,为什么这么分配呢?
	private void allocateElements(int numElements) {
		int initialCapacity = MIN_INITIAL_CAPACITY;
		if (numElements >= initialCapacity) {
			initialCapacity = numElements;
			initialCapacity |= (initialCapacity >>> 1);
			initialCapacity |= (initialCapacity >>> 2);
			initialCapacity |= (initialCapacity >>> 4);
			initialCapacity |= (initialCapacity >>> 8);
			initialCapacity |= (initialCapacity >>> 16);
			initialCapacity++;
			if (initialCapacity < 0) // Too many elements, must back off
				initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
		}
		elements = new Object[initialCapacity];
	}
}
]]>
栈算法描述+代码实现 2016-09-21T00:00:00+00:00 flybread http://flybread.github.io//2016/09/25

栈算法描述+代码实现

栈对外提供的方法,比较的简单,也不多。出栈,入栈,取栈顶元素,新建栈。
1. pop
2. push
3. peek
4. search(Java的JDK中提供了这个方法)

具体的实现的代码是:

/**
 * @author zhailz
 *
 * 时间:2016年9月26日 ### 上午8:41:22
 *
 * 栈的逻辑
 *
 */
public class MyStack<E> {

	protected Object[] elementData;
	protected int elementCount;
	private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
	protected int capacityIncrement;
	protected transient int modCount = 0;

	public MyStack(int initialCapacity, int capacityIncrement) {
		if (initialCapacity < 0)
			throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
		this.elementData = new Object[initialCapacity];
		this.capacityIncrement = capacityIncrement;
	}

	public MyStack(int initialCapacity) {
		this(initialCapacity, 0);
	}

	public MyStack() {
		this(10);
	}

	/**
	 * 增加元素
	 */
	public E push(E item) {
		addElement(item);
		return item;
	}

	public synchronized E pop() {
		E obj;
		int len = size();
		obj = peek();
		removeElementAt(len - 1);
		return obj;
	}

	public synchronized void removeElementAt(int index) {
		modCount++;
		if (index >= elementCount) {
			throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
		} else if (index < 0) {
			throw new ArrayIndexOutOfBoundsException(index);
		}
		int j = elementCount - index - 1;
		//数组的复制,这种复制的方法免除了循环的复制的方法
		if (j > 0) {
			// index + 1 :复制开始的位置
			// index :复制到的位置开始
			// j 复制的长度
			System.arraycopy(elementData, index + 1, elementData, index, j);
		}
		elementCount--;
		elementData[elementCount] = null; /* to let gc do its work */
	}

	public synchronized int size() {
		return elementCount;
	}

	public synchronized E peek() {
		int len = size();

		if (len == 0)
			throw new EmptyStackException();
		return elementAt(len - 1);
	}

	private synchronized E elementAt(int index) {
		if (index >= elementCount) {
			throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
		}

		return elementData(index);
	}

	@SuppressWarnings("unchecked")
	private E elementData(int index) {
		return (E) elementData[index];
	}

	public synchronized void addElement(E obj) {
		modCount++;
		ensureCapacityHelper(elementCount + 1);
		elementData[elementCount++] = obj;
	}

	// 保证容量
	private void ensureCapacityHelper(int minCapacity) {
		if (minCapacity - elementData.length > 0)
			grow(minCapacity);
	}

	private void grow(int minCapacity) {
		// overflow-conscious code
		int oldCapacity = elementData.length;
		//如果设置了增长的元素,每次的增长就按照设置的增长,没有的话,直接*2的增长
		int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
		if (newCapacity - minCapacity < 0)
			newCapacity = minCapacity;
		//超大的栈的时候,怎么处理
		if (newCapacity - MAX_ARRAY_SIZE > 0)
			newCapacity = hugeCapacity(minCapacity);
		elementData = Arrays.copyOf(elementData, newCapacity);
	}

	private static int hugeCapacity(int minCapacity) {
		if (minCapacity < 0) // overflow
			throw new OutOfMemoryError();
		return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
	}

	public static void main(String[] args) {
		int[] a1 = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
		int[] a2 = new int[5];
		System.arraycopy(a1, 6, a2, 2, 2);
		System.out.println(Arrays.toString(a2));
	}

}
]]>
计数排序和基数 2016-09-21T00:00:00+00:00 flybread http://flybread.github.io//2016/09/24

计数排序算法描述+代码实现,基数排序算法描述+代码实现

计数排序的说明: 计数排序(Counting sort)是一种稳定的线性时间排序算法。

当输入的元素是n个0到k之间的整数时,它的运行时间是Θ(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。

由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序算法中,能够更有效的排序数据范围很大的数组。

通俗地理解,例如有10个年龄不同的人,统计出有8个人的年龄比A小,那A的年龄就排在第9位,用这个方法可以得到其他每个人的位置,也就排好了序。当然,年龄有重复时需要特殊处理(保证稳定性),这就是为什么最后要反向填充目标数组,以及将每个数字的统计减去1的原因。

/**
 * @author zhailz
 * 计数排序(Counting sort)是一种稳定的线性时间排序算法。
 * 计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置
 */
public class CountSort {

	public static void main(String[] args) {
		//排序的数组
		int a[] = { 100, 93, 97, 92, 96, 99, 92, 89, 93, 97, 90, 94, 92, 95 };
		int b[] = countSort(a);
		for (int i : b) {
			System.out.print(i + "  ");
		}
		System.out.println();
	}

	/*
	 * 	找出待排序的数组中最大和最小的元素
	 * 	统计数组中每个值为i的元素出现的次数,存入数组 C 的第 i 项
	 * 	对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
	 * 	反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
	 * */
	public static int[] countSort(int[] a) {

		int b[] = new int[a.length];

		//获得数组的最大值和最小值
		int max = a[0], min = a[0];
		for (int i : a) {
			if (i > max) {
				max = i;
			}
			if (i < min) {
				min = i;
			}
		}

		//这使得计数排序对于数据范围很大的数组,需要大量时间和内存
		//这里k的大小是要排序的数组中,元素大小的极值差+1
		int k = max - min + 1;
		int c[] = new int[k];
		for (int i = 0; i < a.length; ++i) {
			c[a[i] - min] += 1;//优化过的地方,减小了数组c的大小
		}

		//前面有几个比我小的值
		for (int i = 1; i < c.length; ++i) {
			c[i] = c[i] + c[i - 1];
		}
		PrintUtil.printArray(a);
		PrintUtil.printArray(c);
		for (int i = a.length - 1; i >= 0; --i) {
			//主要是考虑到了数组的中数字的重复性
			c[a[i] - min] = c[a[i] - min] - 1;
			int index = c[a[i] - min];
			b[index] = a[i];//按存取的方式取出c的元素
			PrintUtil.printArray(b);
		}
		return b;
	}

	public static int[] countingSort(int[] A) {
		int[] B = new int[A.length];
		// 假设A中的数据a'有,0<=a' && a' < k并且k=100
		int k = 100 + 1;
		countingSort(A, B, k);
		return B;
	}

	private static void countingSort(int[] A, int[] B, int k) {
		int[] C = new int[k];
		// 计数
		for (int j = 0; j < A.length; j++) {
			int a = A[j];
			C[a] += 1;
		}
		// 求计数和
		for (int i = 1; i < k; i++) {
			C[i] = C[i] + C[i - 1];
		}
		PrintUtil.printArray(A);
		PrintUtil.printArray(C);

		// 整理
		for (int j = A.length - 1; j >= 0; j--) {
			int a = A[j];
			B[C[a] - 1] = a;
			C[a] -= 1;
			PrintUtil.printArray(B);
			PrintUtil.printArray(C);
		}
	}
}

基数排序的说明:


/**
 * @author zhailz
 * 基数排序的时间复杂度是O(k·n),其中n是排序元素个数,k是数字位数。
 * 注意这不是说这个时间复杂度一定优于O(n·log(n)),k的大小取决于数字位的选择(比如比特位数),
 * 和待排序数据所属数据类型的全集的大小;k决定了进行多少轮处理,而n是每轮处理的操作数目。
 */
public class RadixSort {

	/**
	 * 基数排序的发明可以追溯到1887年赫尔曼·何乐礼在打孔卡片制表机(Tabulation Machine)上的贡献。
	 * 它是这样实现的:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,
	 * 从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
	 *
	 * */
	// 求取的数组中最大数的位数
	public static int maxbit(int data[], int n) //辅助函数,求数据的最大位数
	{
		int maxData = data[0]; ///< 最大数
		/// 先求出最大数,再求其位数,这样有原先依次每个数判断其位数,稍微优化点。
		for (int i = 1; i < n; ++i) {
			if (maxData < data[i])
				maxData = data[i];
		}
		int d = 1;
		int p = 10;
		while (maxData >= p) {
			maxData /= 10;
			++d;
		}
		return d;
	}

	public static void radixsort(int data[], int n) //基数排序
	{
		int d = maxbit(data, n);
		int[] tmp = new int[n];
		int[] count = new int[10]; //计数器
		int i, j, k;
		int radix = 1;
		for (i = 1; i <= d; i++) //进行d次排序
		{
			for (j = 0; j < 10; j++)
				count[j] = 0; //每次分配前清空计数器
			for (j = 0; j < n; j++) {
				k = (data[j] / radix) % 10; //统计每个桶中的记录数
				count[k]++;
			}

			for (j = 1; j < 10; j++)
				count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
			for (j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
			{
				k = (data[j] / radix) % 10;
				tmp[count[k] - 1] = data[j];
				count[k]--;
			}
			for (j = 0; j < n; j++) //将临时数组的内容复制到data中
				data[j] = tmp[j];
			radix = radix * 10;
		}
	}

	public static void main(String[] args) {
		int[] data = { 73, 22, 93, 43, 55, 14, 28, 65, 39, 81, 33, 100 };
		RadixSort.radixsort(data, 3);
		for (int i = 0; i < data.length; i++) {
			System.out.print(data[i] + " ,");
		}
	}
}

]]>
桶排序 2016-09-21T00:00:00+00:00 flybread http://flybread.github.io//2016/09/23

桶排序算法描述+代码实现
桶排序以下列程序进行: 1. 设置一个定量的数组当作空桶子。
2. 寻访序列,并且把项目一个一个放到对应的桶子去。
3. 对每个不是空的桶子进行排序。
4. 从不是空的桶子里把项目再放回原来的序列中。

伪代码

function bucket-sort(array, n) is
  buckets ← new array of n empty lists
  for i = 0 to (length(array)-1) do
    insert array[i] into buckets[msbits(array[i], k)]
  for i = 0 to n - 1 do
    next-sort(buckets[i])
  return the concatenation of buckets[0], ..., buckets[n-1]

JAVA实现的代码:

/**
 * @author zhailz
 *
 * 时间:2016年9月22日 ### 上午11:08:16
 * 最差时间复杂度	O(n^{2})
 * 平均时间复杂度	 O(n+k)
 * 最差空间复杂度	 O(n*k)
 */
public class BucketSort {

	public static void main(String[] args) {
		int[] arrays = new int[] { 5, 1, 6, 2, 4, 5, 6, 7, 0, 4, 2, 3, 5, 7, 0, 1, 2, 3, 8 };
		arrays = bucketSort(arrays);
		System.out.println(Arrays.toString(arrays));
	}

	/**
	 * n 为分配的范围,例如我们使用取余的方式,确定数组中的值回落到哪一个位置
	 * */
	public static int[] bucketSort(int arr[]) {
		int max = 0;
		for (int i : arr) {
			if (i > max) {
				max = i;
			}
		}

		int arrl = arr.length - 1;
		ListNode[] nodes = new ListNode[arrl + 1];
		for (int i = 0; i < arr.length; i++) {
			//保证数组中的元素,按照大在前,小在后的不严格顺序的全部的落入到nodes数组中
			int index = arr[i] * arrl / max;
			if (nodes[index] == null) {
				nodes[index] = new ListNode(arr[i]);
			} else {
				//这里使用遍历链表的算法
				for (ListNode temp = nodes[index];
           temp != null; temp = temp.next) {
					if (temp.val >= arr[i]) {
						//换值变为后插入
						ListNode insert = new ListNode(temp.val);
						temp.val = arr[i];
						insert.next = temp.next;
						temp.next = insert;
						break;
					}
					temp = temp.next;
				}
			}
		}

		int j = 0;
		int[] tmp = new int[arr.length];
		for (ListNode listNode : nodes) {
			if (listNode != null) {
				while (listNode != null) {
					tmp[j] = listNode.val;
					listNode = listNode.next;
					j++;
				}
			}
		}

		return tmp;
	}

	public void print(ListNode[] nodes) {

	}
}

]]>