树相关的算法一般都需要递归。
这道题需要特别注意子树的概念,必须是从根节点到叶子结点(包括nil)都要完全匹配,才算是子树。
整体思路就是从被匹配树的根节点开始匹配,匹配不成功,则分别对左子树和右子树进行匹配。这里需要进行递归操作,退出条件就是被匹配的树已经为nil了。
而匹配的过程又是一层递归,先匹配根节点,然后匹配左子树,再匹配右子树。直到匹配到nil节点。
因此这道题最大的难点在于两层递归
通过递归交换左右子树。
两个考察点:一个是树的广度遍历(通过队列实现),还有一个就是如何判断哪些节点在一个层次。
树的广度遍历,首先插入头结点,然后退出条件设置为队列为空的时候,然后左子节点和右子节点不断入队列。
判断层次的话有两种做法:第一种:插入一个nil标记,读到nil的时候说明当前层次遍历完毕,然后将nil重新插回队尾。第二种:在遍历开始前,获悉队列中数据个数,即为当前层次个数,然后循环到次数后进入下一个层次。
这道题直接在上一道题的基础上做改动,设置一个方向flag,指明当前是从左到右遍历,还是从右到左遍历。然后在输出时选择尾插还是头插。
类似上一个思路,将结果头插
这里主要注意两点:
此外,针对除以2的操作,我们采用右移1位。判断奇偶的操作,我们和0x1进行与操作。
链表结构最常用的技巧就是快慢指针。
如果我们从头开始寻找当前节点的前一个节点,那么时间复杂度就是O(n)。 但是题目强调说被删除节点不是尾节点,这意味着当前节点一定有下一个节点。如果我们把下一个节点删除,然后下一个节点的值覆盖当前节点的值,其实效果是一样的。
链表结构的基本功。通过三个指针,第一个指向新链表头,第二个指向旧链表头,第三个指针用来移动旧链表头。
基本思路和上一题类似,只不过我们需要先遍历到m的位置,然后翻转m到n之间的节点,然后再把n之后的节点连接上。
常规的归并排序。
#merge k sorted lists 利用上一题的合并两个链表的方法merge(l1,l2),递归合并merge(l1,list[1:])
最朴素的想法大家都能想到,就是不停地和0x01做与操作,然后无符号右移一位。
这里有个更好的想法,就是n&(n-1),会把最右侧的1变成0.因此能执行几次这个操作,就说明有几个1。
分布式系统和单机系统有很大的差异,分布式系统会面临很多新的故障问题.
单机的程序,如果硬件没有故障,那么相同操作下产生相同的结果.如果硬件出现了故障,那么程序将无法运作.因此对于运行编写良好程序的单机应用,要么完好的工作,要么彻底无法提供服务,不会存在中间的状态.在计算机的设计中有一个深思熟虑的设计:如果内部错误发生了,我们宁愿死机也不能返回错误的结果,因为处理起来,错误的结果很难处理也很困扰人.计算机将底层不确定都屏蔽掉,始终对应用层提供确定性.
在分布式系统里,消息在网络中传播所花费的时间是不确定的,而多个节点故障发生也是不确定的,因此这些不确定性加上部分失效导致分布式系统很难去处理这些问题.
有两种理念来建立大规模的计算系统.
在超级计算机上,一个作业不时的将当前的计算状态持久化到存储系统上,如果一个节点失败了,通常会将整个集群停下,在故障节点恢复后,计算工作通过最近的checkpoint继续进行.因此超级计算机更像是一个单机计算机,将部分失效演变成完全失效.但是我们更关注互联网服务,这跟超级计算机区别很大:
如果我们想使得我们的分布式系统工作,我们必须接受部分失效和建立容错机制.换句话说,我们需要在不可靠的组件上建立可靠的系统.(这个可靠是有限度的,不可能十分完美的可靠).直观上来说,一个系统的可靠程度,取决于系统中最不可靠的组件,事实并非如此:
尽管系统可以比它所依赖的组件更可靠,但是可靠性是有限度的.纠错码只能纠正少量的单字节错误,TCP也没有办法解决延迟问题.这些系统并不完美,但是通过屏蔽底层的一些错误,遗留的错误很容易被定位和解决.
我们重点是关注无共享分布式系统,系统内的机器通过网络互联,网络也是系统间通信的唯一手段,每个机器拥有自己的内存和硬盘,但是机器之间无法相互访问内存和硬盘,只能通过网络访问彼此的服务接口.无共享不是构建系统的唯一方式,但这已经成为构建互联网服务的主导方式:
互联网和大部分数据中心的内部网络(通常是以太网),都是异步包网络(asynchronous packet networks).这种网络环境下,网络不提供保证数据包什么时间到达目的地,数据包是否能到达,如果你发送了请求然后等待响应,可能会有很多情况发生:
发送方甚至不清楚消息到底发送出去没.通常通过超时机制来解决.但实际上即使是超时了,你也不清楚到底发生了什么.
网络故障时刻存在,即使是在公司维护的数据中心里,人为配置错误是主要的诱因.而共有网络里,可能交换机软件升级导致了数据延迟,鲨鱼咬断了海底光缆,甚至出现接收不到数据,但是能发送数据,因为网络链路不保证双工运行.
网络分区:当网络的一部分和另一部分无法连接,这被称为网络分区或者网路断裂.我们统称为网络故障.
处理网络故障并不意味着要容忍故障:如果你的网络相当可靠,那么处理方式可以是简单的将错误信息反馈给用户.但是你需要了解系统面临网络故障时的反应,以及需要确保系统能够从网络故障里恢复.
需要系统需要自动的检测故障节点.
然而网络使得检测故障节点很困难,在一些特定的场景下,可能会得到一些直白的反馈来告诉你节点是否还在工作:
快速获悉远程节点宕机是很有用的,但你不能指望它.通常情况下,当发生故障时,你会什么响应也没有.你可以重试若干次等到超时,然后猜测远程节点已经宕机了.
如果超时是我们唯一检测故障的手段,那么超时时间应该设置多久呢?很不幸没有简单的答案.
过早的声明一个节点失效了是有问题的,这会造成同样的操作被执行两次.而且当节点被声明失效了,那么其他节点将会接替该节点的工作,这会给其他节点增加负担.如果当前系统正在经历高负荷,而过早地声明一个失效了会加重这种境地.通常情况下节点都只是变慢了而没有完全是失效,这样一来会造成级联故障,所有节点都发觉其他节点因为变慢而声明他们都宕机了,于是整个系统都不工作了.
当网络延迟时间是有保证的时候,我们可以设定一个合理的超时时间.然而目前的异步网络遵循的是尽快的发包,但不保证发送时间的上限.
TCP会在一定超时范围内,将尚未确认的包认为已经丢失,超时时间通过之前观察到的往返之间来估算.丢失的包被自动重新发送,尽管应用层看不到丢包和重发,但是能看到的直观的结果就是时延.
当系统接近最大容量时,排队时延会非常大.拥有大量备用容量的系统可以很快的消耗队列,然而在高度被利用的系统中,长队列很快就会排起来.
在公有云和多租户机房里,资源是被许多消费者共享的.批量任务例如MapReduce很容易就把网络链路吃满.由于无法控制共享资源中其他用户的资源使用,如果你周围的节点使用了很多资源,那么网络延迟时间更加波动.
在这种环境下,只能摸索着选择超时时间.通过较长时间内,跨多台机器之间来观察网络往返的时间分布,最终来决定延迟的可预期变化.然后再考虑应用程序的特征,在故障检测延迟和过早超时风险之间进行权衡.更好的方法就是不去配置固定超时,而是根据响应时间动态的调整超时.TCP就是这么做的.
####TCP和UDP 一些对延迟敏感的应用会采用UDP,而不是TCP.这其实是在可靠性和延迟波动间做出的权衡.因为udp不会进行流量控制也不会重发丢失的包,因此避免了一部分网络延迟.udp适用于丢包延迟数据无所谓的场景,例如ip电话.
如果分布式系统是部署在最大时延固定并且不丢包的网络环境下,问题将简单的多.那么为什么我们不部署在这种硬件环境呢?这里需要对比数据中心的网络和传统固定线路的电话网络.
当我们通过电话网络打电话时,首先需要建立一个电路:一个固定的,保证足够带宽,贯穿两端的线路,这个电路直到通话结束才关闭.这种网络是同步的,传输中的数据不需要排队,带宽固定因此时延固定,我们称之为有界时延.而传统tcp不同,一个电路具有固定带宽则独立使用,而tcp则是尽可能的使用网络带宽,当进行传输大小变化的数据块时,会尽量在最短的时间发送出去,并且当tcp连接空闲时是不占用带宽的.以太网和IP协议都是包交换协议,并没有电路的概念.
那么为什么会这么选择呢,这是为了突发流量做出的优化,因为打电话期间每秒的流量是固定的,而对于互联网来说,时刻发送的数据都是不固定的,因此需要尽可能的发送出去.如果我们通过电路传输数据,就需要提前申请带宽:如果申请小了,那么传送的速度太慢了,导致网络能力被浪费.如果申请的多了,电路建立不起来(因为没有足够的带宽支持).因此采用电路传送突发的流量,会导致网络能力没有被充分利用从而速度很慢.像TCP会动态的调整传送速度来利用网络的能力.
也有混合两种网络的网络,叫做ATM.无线带宽技术有些类似:它实现了链路层上端到端的流量控制,从而减少了网络排队的需求,但是仍然会经历链路拥塞的延迟.利用Qos(quality of service,包的优先级和调度)和接纳控制(admission control, 限速的发送方),能够在包网络中模拟电路交换,从而提供有界的延迟.但是在多租户数据中心和公有云,或者互联网通信场景下,这种服务质量还没有启用.
更一般的讲,我们可以吧变动的延迟看作是动态资源分区的结果.电路交换是一种静态分配方式,就算只有一通电话在拨打,其他的带宽也没有被利用.包交换网络是一种动态分配方式,数据被尽可能快的发送出去,这种方式有排队的缺点,但是能够最大化利用带宽.
在cpu利用上有相同的境遇.如果在多线程中动态的共享cpu核,那么线程之间需要互相等待和排队,因此挂起时间无法预测.但是硬件的利用率要比你给线程分配固定数量的cpu周期要好.更好的硬件利用率是采用虚拟机的动机.
在静态资源分配下,延迟是有保证的.但是也降低了利用率,换言之就是代价太大.而动态资源分配下,资源利用率提高了,但是延迟也变得不可控.
所以,变化的延迟不是自然产生的,而是代价/收益权衡的结果.
对时间的利用主要是利用时间段和时间点.网络中的每台机器都有自己的时钟,实际上是一种硬件设备,通常是石英晶体振荡器.这种设备并不是完全精确的,因此每台机器都有自己的时间概念,可能快也可能慢.可以在某种程度上同步这个时间,目前最常使用的机制就是NTP(Network Time Protocol):该协议通过一组服务集群提供的时间来调整计算机的时间,而这些集群则通过更准确的时间源——GPS.
现代计算机至少拥有两种不同的时钟:时刻时钟和单调时钟.
时刻时钟和直观上我们认知的时钟一直,返回当前的日期和时间.时刻时钟通常通过NTP进行同步,这意味着两台机器上的时间戳是一致的.但是时刻时钟也有很古怪的地方,例如当前时钟速度快于NTP服务器,某个时刻会被NTP服务器纠正回到之前的某个时刻.这种跳跃和忽略闰秒使得时刻时钟不能用来评估时间流逝.
单调时钟很适合评估时间段,例如超时时间和响应时间.单调时钟顾名思义总能保证时间是向前的,而不像时刻时钟会跳回过去某个时刻.单调时钟能用来评估时间的流逝.但是单调时钟的绝对值是没有意义的,不同计算机之间的单调时钟值没有任何比较的意义.
在多cpu套接字的服务器上,每个cpu有自己的计时器,并且彼此之间不用同步.操作系统会补偿这种差异,并试图赋予应用线程单调的时钟视图,尽管这些线程会调度到不同的cpu上.但是尽量不要太依赖这种单调性保证.
NTP会调整单调时钟的移动速率,但是不会影响单调性,而且单调性时钟的精度通常很高,能够精确到微秒甚至更小.在分布式系统里,单调时钟适用于度量时间的流逝,因为不需要考虑不同节点时钟的同步.并且对微小的差异不敏感.
单调时钟不需要同步,但是时刻时钟需要借助外部时间源来同步,但也没有我们想象的那么精确:
获取高精度时钟是可能的,需要投入大量的资源.
时钟问题有部分原因是因为很容易被忽略:如果cpu有缺陷或者网络被错误配置,机器不大可能正常工作,因此会很快被发现和修复.但是如果石英钟有缺陷或者NTP客户端配置错误,大部分事情看起来都是好的,尽管时钟在逐渐的漂移离实际时间越来越远.如果有软件依赖精确的时钟同步,那么结果可能是沉默或者微秒的数据丢失,而不是系统崩溃.
因此当你的应用比较依赖同步的时钟时,必须要监控所有机器上时间的偏差.任何偏差较大的节点都需要从集群摘除,这样能避免损失.
LWW的根本问题:
我们无法准确的定义最新的数据,而NTP的同步精度也受到网络延迟的限制.因此一般基于自增长计数的逻辑时钟被采用.
公网上通过NTP服务同步时间,精度可能是几十ms,而当网络拥堵时可能会高达100ms.因此时钟的读数往往不意味着某个时刻,更应该被认为是一个时间区间.这个区间的边界取决于你的时间源,如果是GPS接收器或者原子时钟,那么生产商会提供.如果是通过NTP服务,那么就需要将自上次同步后的石英漂移+NTP服务器不确定+网络往返时间.不过大部分系统并不提供一个不确定的区间给你参考.
谷歌的Spanner里包含TrueTime的API,明白地提供本地时钟的置信区间,一般返回[earliest,latest],分别表示最早的可能时间戳和最晚的可能时间戳.
快照隔离性中,最常见是通过单调的递增事务ID.在单机系统里,简单的计数器就足够了.但是分布式数据库中,一个全局的单调的递增事务ID就很难产生了,因为需要协调.由于许多小而且快速的事务,因此分布式系统产生事务ID成为了系统瓶颈.
Spanner通过TrueTime返回始终置信区间来实现快照隔离性.对于A = [A_earliest, A_latest] and B = [B_earliest, B_latest],如果两个区间没有重叠,例如A_earliest <= A_latest < B_earliest <= B_latest,那么就可以说B清楚地发生在A之后.为了确保时间戳能反映出因果关系,Spanner会在提交读写事务前有意等待置信区间的长度.那么就需要确保置信区间的长度足够短,Google在每个数据中心部署了GPS接收器或者原子时钟,使得时钟被同步在7ms以内.
有这样一种场危险的时钟使用场景在分布式系统中,单主架构里,leader如何确保自己还是leader呢?一种选择是从其他节点获取租约,流程大体如下:
while (true) {
request = getIncomingRequest();
// Ensure that the lease always has at least 10 seconds remaining
if (lease.expiryTimeMillis - System.currentTimeMillis() < 10000) {
lease = lease.renew();
}
if (lease.isValid()) {
process(request);
}
}
这里是有问题的,如果使用时刻时钟,那么超时时间戳和本地时间戳是不同机器给出的机器,如果时间同步有异常,这种策略是有问题的.
及时我们采用本地单调时钟,会有另外一个问题.因为获取当前时间戳和处理请求之间我们一般认为会运行的很快,因此设定10s的过期时间是足够的.但是如果程序执行中出现了进程暂停呢?比如在处理请求时暂停了15s,而恢复运行后线程并不知道被暂停了那么久,而此时处理请求其实是不安全的.线程暂停的因素很多:
任意时间的发生都会抢占线程的运行,然后后续再恢复运行.在单机上可以通过同步等手段来协调多线程程序运行,但在分布式环境下,当一台机器上的进程暂停了,其他机器上的进程还在继续运行着,甚至认为该机器已经死机了.
存在一些系统需要保证某个时刻必须响应,如果不响应将会造成整个系统的失败,这种被称为强实时(hard real-time)系统.在嵌入式系统中,实时意味着系统被小心设计和测试,从而能够在任何场景下满足特殊的时间保证.而在web场景下,意味着服务端向客户端推送以及没有硬性响应时间约束的流处理过程.
提供实时保证需要各个级别的支持:实时操作系统,在某段时间内保证cpu时间的前提下,允许进程被调度.库函数需要说明最坏的执行时间.动态内存申请需要被限制甚至被取消.大量的测试和评估工作必须被执行来确保满足了条件.
实时系统的实现代价很大,往往因为实时性导致吞吐量较低.对于服务端数据处理系统,提供实时保证是不经济的.因此分布式系统依然要忍受随时的暂停和时钟不稳定.
不一定非要昂贵的实时调度保证,进程暂停的负面影响也可以减缓些.当编程语言运行时计划gc时,这个过程是可变动的.一种新型的想法是,把gc看做一个简单计划好的暂停节点动作.当一个节点将要gc时,运行时会发送请求给应用,应用层会停止发送新的请求到该节点上,该节点处理完尚未结束的请求,然后就可以在没有请求的环境下进行gc了.这种方案屏蔽了暂停并且降低了响应延迟.许多延迟敏感的财务交易系统就采用这种方式.
另外一个变种是,对短期对象(可快速回收)执行垃圾回收,然后在积累足够多的长期存活对象以至于需要full gc之前,定期进行系统重启.一次只有一个节点进行重启,重启时流量被切换到其他节点上,这类似轮转升级.
这些方案都不能避免gc停顿,但能够减少对应用的影响.
分布式系统不同于单机:没有共享的内存,通过没有固定延迟的不可靠网络通信,会遭受部分失效,不可靠的时钟和进程暂停.
网络中的节点不可能准确的知道一切事情,只能基于通过网络接收到的信息(或者接收不到信息)来进行猜测.节点只能通过在网络中交换信息来获悉其他节点的状态.如果远程节点没有回复,那么将无从知晓它的状态,因为没办法区分是网络问题还是节点自身的问题.
在分布式系统中,我们可以针对我们的系统模型做出假设,然后以满足这些假设的方式设计出实际的系统.即使底层不可靠的系统模型提供较少的保证,我们依然可以得到可靠的行为.尽管在一个不可靠的系统模型上使得我们的软件运行良好是可能的,但这并不容易做到.因此我们需要知道我们能做哪些假设,以及我们需要提供哪些保证.
因此一个节点一定不能相信自己对某个情况的判断.许多分布式算法都依赖于quorum机制,也就是节点投票:为了减少对任意一个节点的依赖,需要来自若干节点的最小投票数来做决定.
大部分情况下,quorum是超过半数节点的绝大多数,其他类型的quorum也有在用.
如果一个节点继续以自封的leader身份发送消息,在设计不良好的系统中其他节点认可了这个消息,就会造成系统运行不正常.Hbase中曾经出现过这种bug:

这种bug是因为租约未到期前,发生了较长时间的gc停顿,然后租约过期了.gc结束后该节点恢复过来,认为租约未过期并且自己仍然有写权限,从而导致了异常.
为了解决上述问题,提出了fencing令牌的方法:
当从锁服务器获取锁或者租约时,也会返回一个fencing令牌,这个令牌是自增长的.任何客户端发送写请求时必须包含该令牌.这样,当新的获得租约的客户端和旧租约客户端冲突时,较大令牌值的客户端胜出.zk使用zxid(事务id)或者cversion(节点版本)作为fencing令牌.
这个机制需要资源自身参与到解决令牌冲突的过程中.如果存储层不支持,那么就只能有限制的使用这种方法(比如在文件名中包含令牌id).对于服务端来说,增加校验令牌的工作不见得是一件坏事情.
fencing令牌可以避免非故意的错误.但是如果一个节点故意想要破坏系统的保障,比如使用一个伪造的fencing令牌.目前我们都认为节点是不可靠的,但是很诚实.如果分布式环境中,节点故意伪造信息那么分布式问题会变得更难.在一个不可信的环境里达成一致,称为拜占庭将军问题.
如果一个系统在即使节点出故障并且不遵守协议的情况下,甚至有人恶意攻击干扰网络,仍然能够正确运行,这个系统被称为拜占庭容错的:
但是我们现在不考虑拜占庭错误.在我们自己的数据中心里,节点都是我们自己的机构控制,辐射也足够低因此也不会造成内存数据出错.拜占庭容错的协议极其复杂而且一来硬件设备,大部分服务端系统中,拜占庭容错的部署方案是不切实际不可行的.我们的系统应该预期来自终端用户的输入会有各种各样的异常,因此我们才会做输入校验,数据清理,输出转义来避免sql注入和跨网站脚本攻击.但是,我们并不适用拜占庭容错协议,只是简单的让服务器决定哪些行为允许哪些不允许.拜占庭容错更多是和p2p环境相关.
程序的bug可以被看做拜占庭错误,但是因为你部署在了所有的节点,所以拜占庭容错协议也毫无办法.大部分拜占庭容错协议需要超过2/3的大多数,也就是说之多1/4的节点出现bug.
如果一个协议能够保护我们避免恶意攻击那很好,但是这不现实.因为攻击者能够攻击一个节点就能攻击所有的节点.因此传统的机制(授权,访问控制,加密,防火墙等等)仍然是对抗攻击者的主要保护措施.
尽管我们假定节点通常是诚实的,但还是要采取一些措施还对空弱化的谎言.比如,由于硬件故障导致的错误消息,软件bug和错误配置.这些措施不是完全拜占庭容错的:
解决分布式问题的算法,需要在不过分依赖硬件和软件配置的基础上来实现.因此我们需要形式化我们在系统中可能遇到的错误,我们称之为系统模型,这是对我们算法可能遇到的问题进行的一种抽象.
关于时间议题,主要有三种模型:
关于节点议题,也有三种模型:
现实中,包含Crash-recovery faults的部分同步模型是最有用的.
我们说算法是正确的这意味着什么呢?如果一个算法是正确的,它需要满足一些特质.例如我们生成fencing令牌,那么我们需要算法提供这些特质:
因此如果在某些系统模型下,在这些模型限制下发生的所有异常场景下,算法都能满足这些特征,我们说算法就是正确的.但这毫无意义,因为一旦所有节点都崩溃了,算法什么也做不了了.
需要区分两类特征:安全性和活跃性,上述例子里,唯一性和单调序列是安全性,可用性是活跃性问题.一般带有最终字样的定义,都是活跃性问题.
安全性:坏的事情不会发生.
活跃性:正确的事情最终一定会发生.
区分安全性和活跃性能够帮助我们处理复杂的系统.对于分布式算法,安全性必须被保证.就算所有机器都宕机了,至少不会返回一个错误的结果.对于活跃性我们可以给出一些警告,只有当大部分节点都正常,并且网络最终会恢复的情况下,请求可以收到一个响应.
在推理出分布式算法的正确性上,安全性,活跃性和系统模型很有用.但是系统模型毕竟还是现实世界的简化.
比如crash-recovery模型中假设通过稳定存储里的数据恢复,但是假如这些数据也丢失了呢?系统模型只是第一步,实际中产生的错误还是需要理论分析和海量的测试.
分布式环境下我们会遇到的问题:
部分失效是分布式系统的典型特征.在分布式系统,必须建立部分失效容错的机制,使得系统作为整体还是可以继续工作的.
建立容错机制,第一步就是检测,但是这很难.大部分系统没有准确的机制来检测节点是否失败了,大部分分布式算法依赖超时机制.但是超时不能区分究竟是网络问题还是节点问题
一旦检测出故障,容忍故障也不容易:没有全局变量,也没有共享内存,机器之间也没有共享机器状态的能力.节点之间甚至都不能就时间达成一致,唯一的方式就将交互信息在不可靠的网络上.主要的决策不能依赖单个节点,需要多个节点共同决定.
分布式系统在扩展性,容错和低延迟上都比单节点有优势.
利用两个栈结构,两次先进后出=>先进先出.
这里其实就是利用遍历的方式,获取到最后一个元素,然后将之前的遍历放到另一个空队列,之后将交换两个队列的地位。
这道题比较容易,利用双栈,一个用来存数一个用来存当前最小值。
需要注意的是当第一次push时,此时是没有最小值的。