蒋海云个人博客,日拱一卒。 2018-04-03T11:03:34+08:00 jiang.haiyun#gmail.com 小程序框架基础 2018-04-03T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/miniprogram-framework 小程序包含一个描述整体程序的 app 和多个描述各自页面的 page。

app.jsonwindow 对象中的配置顶:

  • navigationBarBackgroundColor: 导航栏背景色,HexColor,默认为 #000000, 黑色。
  • navigationBarTextStyle: 导航栏标题颜色,默认为 white,仅支持 white/black
  • navigationBarTitleText: 导航栏标题文字内容
  • navigationStyle: 导航栏样式,仅支持 default/custom。默认为 default,当为 custom 模式时可自定义导航栏,只保留右上角胶囊状的按钮, V6.6.0 起。
  • backgroundColor: 窗口背景色,默认为 #ffffff,白色。
  • backgroundTextStyle: 下拉 loading 的样式,仅支持 dark/light, 默认为 dark。
  • backgroundColorTop: 顶部窗口的背景色,仅 iOS 支持,默认为 #ffffff,自 v6.5.16。
  • backgroundColorBottom: 底部窗口的背景色,仅 iOS 支持,默认为 #ffffff,自 v6.5.16。
  • enablePullDownRefresh: 是否开启下拉刷新,默认为 false。
  • onReachBottomDistance: 页面上拉后触发触底事件时距页面底部距离,整型,默认为 50,单位为 px。

app.jsontabBar 对象中的配置顶:

  • color: tab 上的文字默认颜色。
  • selectedColor: tab 上的文字选中时的颜色。
  • backgroundColor: tab 的背景色。
  • borderStyle: tabbar 边框的颜色,仅支持 black/white,默认为 black。
  • list: tab 的列表,为一个 Array,最小 2 个,最多 5 个 tab。
  • position: 可选值为 bottom, top,默认为 bottom,当为 top 时,将不会显示 icon。

list 中的列表元素配置顶:

  • pagePath: 页面路径,必须在 pages 中先定义。
  • text: tab 上按钮文字。
  • iconPath: 图片路径,icon 大小限制为 40kb,建议尺寸为 81x81 px,不支持网络图片。
  • selectedIconPath: 选中时的路径。

每个页面的 page.json 配置文件用来配置本页面的窗体 UI,里面的配置项将覆盖 app.json 中的 window 对象中的相同配置项。

其中新增的配置项有:

  • disableScroll: 设置页面不能上下滚动,只在 page 中有效,默认为 false。

逻辑层 App Service

在 JS 的基础上,有如下修改:

  • 增加 AppPage 方法,进行程序和页面的注册。
  • 增加 getAppgetCurrentPages 方法,分别用来获取 App 实例和当前页面栈。
  • 提供 API,获取微信用户数据,扫一扫,支付等微信功能。
  • 每个页面有独立的作用域,并提供模块化能力。
  • 由于框架并非运行在浏览器中,故 document, window 等无法使用。
  • 编写的所有代码最终会打包成一份 JS。

注册程序

App

App() 函数用来注册一个小程序。接受一个 object 参数,用来指定小程序的生命周期函数等。

  • onLaunch: 生命周期函数,当小程序初始化完成后,触发(全局只触发一次)
  • onShow: 生命周期函数,当小程序启动,或从后台进入前台显示后,触发
  • onHide: 生命周期函数,当小程序进入后台后,触发
  • onError: 错误监听函数,当小程序发生脚本错误,或者 api 调用失败时,会触发 onError 并带上错误信息
  • 其它:开发者可添加任意的函数,并通过 this 访问 App 实例。

例如:

App({
  onLaunch: function(options) {
    // Do something initial when launch.
  },
  onShow: function(options) {
      // Do something when show.
  },
  onHide: function() {
      // Do something when hide.
  },
  onError: function(msg) {
    console.log(msg)
  },
  globalData: 'I am global data'
})

onLaunch, onShow 的参数有:

  • path: 打开小程序的来源路径, String
  • query: 打开小程序的 query 信息,Object
  • scene: 打开小程序的场景值,Number
  • shareTicket: String
  • referrerInfo: Object, 当小程序是通过另一个小程序、或公众号或 App 打开时,返回该字段
  • referredInfo.appId: 来源小程序、或公众号或 App的 appid
  • referredInfo.extraData: 来源小程序传来的数据,当 scene=1037 或 1038 时支持

getApp()

全局的 getApp() 函数返回小程序实例对象:

// other.js
var appInstance = getApp()
console.log(appInstance.globalData) // I am global data

注意:

  • App() 必须在 app.json 中注册,且不能注册多个。
  • 不要在定义于 App() 内的函数中调用 getApp(),使用 this 就可拿到 app 实例。
  • 不要在 onLaunch 中调用 getCurrentPages(),此时 page 还没有生成。
  • 通过 getApp() 获取实例后,不要私自调用生命周期函数。

注册页面

Page() 函数用来注册一个页面。接受一个 object 参数,指定页面的初始数据,生命周期函数,事件处理函数。

object 参数有:

  • data: 页面原初始数据,Object。
  • onLoad: 生命周期函数,监听页面加载。一个页面只会调用一次,可在其中获取打开当前页所调用的 query 参数。
  • onUnload: 生命周期函数,监听页面卸载。当 redirectTonavigateBack 时调用。
  • onReady: 生命周期函数,监听页面初次渲染完成。一个页面只会调用一次,代表页面已经准备妥当,可以和视图层进行交互。对界面的设置如 wx.setNavigationBarTitle 请在 onReady 之后设置。
  • onShow: 生命周期函数,监听页面显示。
  • onHide: 生命周期函数,监听页面隐藏。当 navigateTo 或 tab 切换时调用。
  • onPullDownRefresh: 页面相关事件处理函数,监听用户下拉动作。需开启 enablePullDownRefresh。当处理完数据刷新后, wx.stopPullDownRefresh 可停止当前页面的下拉刷新(关闭刷新动画)。
  • onReachBottom: 页面上拉触底事件的处理函数。在触发距离内滑动期间,本事件只会被触发一次。
  • onShareAppMessage: 用户点击右上角转发时调用。只有定义了此事件处理函数,右上角菜单才会显示 “转发” 按钮。此事件需返回一个 Object,用于自定义转发内容。
  • onPageScroll: 页面滚动事件的处理函数。参数为一 Object,字段有 scrollTop,表示页面在垂直方面已滚动的距离(px)。
  • onTabItemTap: 当前是 tab 页时,点击 tab 时触发。
  • 其它: 开发者可添加任意的其它函数,用 this 可访问本对象。

object 的内容在页面加载时会进行一次深拷贝,故需考虑数据大小对加载的影响,例如:

//index.js
Page({
  data: {
    text: "This is page data."
  },
  onLoad: function(options) {
    // Do some initialize when page load.
  },
  onReady: function() {
    // Do something when page ready.
  },
  onShow: function() {
    // Do something when page show.
  },
  onHide: function() {
    // Do something when page hide.
  },
  onUnload: function() {
    // Do something when page close.
  },
  onPullDownRefresh: function() {
    // Do something when pull down.
  },
  onReachBottom: function() {
    // Do something when page reach bottom.
  },
  onShareAppMessage: function () {
   // return custom share data when user share.
  },
  onPageScroll: function() {
    // Do something when page scroll
  },
  onTabItemTap(item) {
    console.log(item.index)
    console.log(item.pagePath)
    console.log(item.text)
  },
  // Event handler.
  viewTap: function() {
    this.setData({
      text: 'Set some data for updating view.'
    }, function() {
      // this is setData callback
    })
  },
  customData: {
    hi: 'MINA'
  }
})

初始化数据

作为页面的第一次渲染使用,data 中的数据必然能 JSON 化,如有字符串、数字、布尔值、对象、数组等。 这些数据要在模板中进行绑定:

//初始化数据
Page({
  data: {
    text: 'init data',
    array: [{msg: '1'}, {msg: '2'}]
  }
})

模板中绑定:

<view></view>
<view></view>

自定义转发字段

onShareAppMessage() 返回一个 Object,用于自定义转发内容。自定义转发字段有:

  • title: 转发标题,默认值为当前小程序名称。
  • path: 转发路径,默认值为当前页面 path,必须是以 / 开头的完整路径。

例如:

Page({
  onShareAppMessage: function () {
    return {
      title: '自定义转发标题',
      path: '/page/user?id=123'
    }
  }
})

Page.prototype.route 字段可以获取到当前页面的路径。

setData()

数据绑定功能类似 React,data 中的数据项绑定到视图,当用 setData() 更新 data 后,视图中的绑定的对应部分也自动更新。

Page.prototype.setData() 函数用于将数据从逻辑层发送到视图层(异步),同时改变对应的 this.data 的值(同步)。

setData() 参数:

  • data: Object 类型,必填,为这次要改变的数据。
  • callback: Function 类型,可选填,在 setData 对界面渲染完毕后回调。

object 中以 key, value 形式表示。其中 key 可非常灵活,以数据路径的形式给出,如 array[2].message, a.b.c.d,并且不需要在 this.data 中预先定义。

注意:

  • 直接修改 this.data 而不调用 this.setData 是无法改变页面的状态的,还会造成数据不一致。
  • 单次设置的数据不能超过 1024k,尽量避免一次设置过多数据。
  • 不要将 data 中任何一项的 value 设为 undefined。

示例:

<!--index.wxml-->
<view></view>
<button bindtap="changeText"> Change normal data </button>
<view></view>
<button bindtap="changeNum"> Change normal num </button>
<view></view>
<button bindtap="changeItemInArray"> Change Array data </button>
<view></view>
<button bindtap="changeItemInObject"> Change Object data </button>
<view></view>
<button bindtap="addNewField"> Add new data </button>
//index.js
Page({
  data: {
    text: 'init data',
    num: 0,
    array: [{text: 'init data'}],
    object: {
      text: 'init data'
    }
  },
  changeText: function() {
    // this.data.text = 'changed data'  // bad, it can not work
    this.setData({
      text: 'changed data'
    })
  },
  changeNum: function() {
    this.data.num = 1
    this.setData({
      num: this.data.num
    })
  },
  changeItemInArray: function() {
    // you can use this way to modify a danamic data path
    this.setData({
      'array[0].text':'changed data'
    })
  },
  changeItemInObject: function(){
    this.setData({
      'object.text': 'changed data'
    });
  },
  addNewField: function() {
    this.setData({
      'newField.text': 'new data'
    })
  }
})

Page 的生命周期:

mina-lifecycle.png

路由

所有页面的路由都由框架统一管理,并以栈形式维护当前的所有页面。当发生路由切换时,页面栈操作如下:

路由方式 页面栈表示
xx  
初始化 新页面入栈
打开新页面 新页面入栈
页面重定向 当前页面出栈,新页面入栈
页面返回 页面不断出栈,直到目标返回页,新页面入栈
Tab 切换 页面全部出栈,只留下新的 Tab 页面
重加载 页面全部出栈,只留下新的页面

getCurrentPages() 获取当前页面栈的实例,以数组形式按栈的顺序给出,第一个是首页(栈尾),最后一个为当前页面(栈顶)。

路由的触发方式:

路由方式 触发时机 路由前页面回调 路由后页面回调
xx      
初始化 小程序打开的第一个页面   onLoad, onShow
打开新页面 调用 wx.navigateTo() 或使用组件 <navigator open-type="navigateTo"/> onHide onLoad, onShow
页面重定向 调用 wx.redirectTo() 或使用组件 <navigator open-type="redirectTo"/> onUnload onLoad, onShow
页面返回 调用 wx.navigateBack() 或使用组件 <navigator open-type="navigateBack"/> 或用户按左上角返回按键 onUnload onShow
Tab 切换 调用 wx.switchTab() 或使用组件 <navigator open-type="switchTab"/> 或用户切换 Tab    
重加载 调用 wx.reLaunch() 或使用组件 <navigator open-type="reLaunch"/> onUnload onLoad,onShow
  • navigateTo, redirectTo 只能打开非 tabBar 页面
  • switchTab 只能打开 tabBar 页面
  • reLaunch 可打开任意页面
  • 页面底部的 tabTab 由页面决定,即只要是定义为 tabBar 的页面,底部都有 tabBar。
  • 调用页面路由带的参数可在目标页面的 onLoad 中提取。

模块化

文件作用域

变量和函数只在本文件中有效。通过全局函数 getApp() 可获取全局的应用实例,并将全局数据设置其中,如:

// app.js
App({
  globalData: 1
})

// a.js
// The localValue can only be used in file a.js.
var localValue = 'a'
// Get the app instance.
var app = getApp()
// Get the global data and change it.
app.globalData++


// b.js
// You can redefine localValue in file b.js, without interference with the localValue in a.js.
var localValue = 'b'
// If a.js it run before b.js, now the globalData shoule be 2.
console.log(getApp().globalData)

模块化

可将公用代码抽离成为一个单独 js,作为一个模块。模块只有通过 module.exportsexports 才能对外暴露接口。其中 exports 其实是对 module.exports 的一个引用,故不能更改其指向。

例如:

// common.js
function sayHello(name) {
  console.log(`Hello ${name} !`)
}
function sayGoodbye(name) {
  console.log(`Goodbye ${name} !`)
}

module.exports.sayHello = sayHello
exports.sayGoodbye = sayGoodbye

在需要使用该模块的文件中,用 require() 引入:

var common = require('common.js')
Page({
  helloMINA: function() {
    common.sayHello('MINA')
  },
  goodbyeMINA: function() {
    common.sayGoodbye('MINA')
  }
})

require() 暂不支持绝对路径。

视图层

模板语言 WXML

Mustache 语法(``) 不仅进于绑定数据,还用于表达式值,因此还用在 wx:for, wx:if 等语句中。

数据绑定

<!--wxml-->
<view>  </view>

// page.js
Page({
  data: {
    message: 'Hello MINA!'
  }
})

列表渲染

<!--wxml-->
<view wx:for="">  </view>

// page.js
Page({
  data: {
    array: [1, 2, 3, 4, 5]
  }
})

条件渲染

<!--wxml-->
<view wx:if=""> WEBVIEW </view>
<view wx:elif=""> APP </view>
<view wx:else=""> MINA </view>

// page.js
Page({
  data: {
    view: 'MINA'
  }
})

模板

<!--wxml-->
<template name="staffName">
  <view>
    FirstName: , LastName: 
  </view>
</template>

<template is="staffName" data=""></template>
<template is="staffName" data=""></template>
<template is="staffName" data=""></template>



// page.js
Page({
  data: {
    staffA: {firstName: 'Hulk', lastName: 'Hu'},
    staffB: {firstName: 'Shang', lastName: 'You'},
    staffC: {firstName: 'Gideon', lastName: 'Lin'}
  }
})

事件

事件可以绑定在组件上,当达到触发事件,就会执行逻辑层中对应的事件处理函数。事件对象可以携带额外信息,如 id, dataset, touches。

例如:

//wxml 中
<view id="tapTest" data-hi="WeChat" bindtap="tapName"> Click me! </view>

//js 中
Page({
  tapName: function(event) {
    console.log(event)
  }
})

事件处理函数的参数是 event, 可以看到 Log 出来的信息大致为:

{
"type":"tap",
"timeStamp":895,
"target": {
  "id": "tapTest",
  "dataset":  {
    "hi":"WeChat"
  }
},
"currentTarget":  {
  "id": "tapTest",
  "dataset": {
    "hi":"WeChat"
  }
},
"detail": {
  "x":53,
  "y":14
},
"touches":[{
  "identifier":0,
  "pageX":53,
  "pageY":14,
  "clientX":53,
  "clientY":14
}],
"changedTouches":[{
  "identifier":0,
  "pageX":53,
  "pageY":14,
  "clientX":53,
  "clientY":14
}]
}

事件分类

  1. 冒泡事件:当一个组件上的事件被触发后,会向父节点传递。
  2. 非冒泡事件:不会向父节点传递。

冒泡事件有:

  • touchstart: 表示手指触摸动作开始
  • touchmove: 表示手指触摸后移动
  • touchcancel: 手指触摸动作被打断,如来电提醒,弹窗
  • touchend: 手指触摸动作结束
  • tap: 手指触摸后马上离开
  • longpress: 手指触摸后,超过 350ms 后再离开,若指定了该事件回调,则 tap 事件将不被触发
  • longtap: 推荐使用 longpress 代替
  • tansitionend: 会在 WXSS transition 或 wx.createAnimation 动画结束后触发
  • animationstart: 会在一个 WXSS animation 动画开始时触发
  • animationiteration: 会在一个 WXSS animation 一次迭代结束时触发
  • animationend: 会在一个 WXSS animation 动画完成时触发
  • touchforcechange: 在支持 3D Touch 的 iPhone 设备中,重按时触发

除以上之外的其它组件自定义事件一般都是非冒泡事件,如 <form/>submit 事件, <input/>input 事件, <scroll-view/>scroll 事件。

事件绑定的写法

类型属性绑定,以 key, value 形式完成。

key 以 bindcatch 开头,然后跟事件类型,如 bindtap, catchtouchstart。自基础库 1.5.0 起,bindcatch 后可紧跟一个 :,如 bind:tap, catch:touchstart

value 是一个字符串,表示在 Page 中定义的函数名。

bind 事件绑定不会阻止冒泡事件向上冒泡,而 catch 会。

事件对象

BaseEvent 对象属性列表:

  • type: 事件类型,String
  • timeStamp: 事件生成时的时间戳,为页面打开到触发事件所经过的毫秒数,Integer
  • target: 触发事件的组件的一些属性值集合,Object,属性值有事件源组件 id,当前组件的标签名 tagName,事件源组件上由 data- 开头自定义属性组成的集合 dataset
  • currentTarget: 当前组件的一些属性值集合,值或同 target, 或为其父组件(冒泡时)

CustomEvent 自定义事件对象属性列表(继承 BaseEvent):

  • detail: 额外的信息,Object,如表单组件提交事件会携带用户的输入。

TouchEvent 触摸事件对象属性列表(继承 BaseEvent):

  • touches: 当前停留在屏幕中的触摸点信息的数组,Array
  • changedTouches: 当前变化的触摸点信息的数组, Array

Touch 对象属性:

  • identifier: 触摸点的标识符,Number
  • pageX, pageY: 距离文档左上角的距离
  • clientX, clientY: 距离页面可显示区域(屏幕除去导航条)左上角距离。

CanvasTouch 对象属性:

  • identifier: 触摸点的标识符,Number
  • x, y: 距离 Canvas 左上角的距离

changedTouches 数据格式同 touches,表示变化的触点,如从无变有 touchstart, 位置变化 touchmove,从有变无 touchend, touchcancel。

脚本语言 WXS

是小程序的一套脚本语言,结合 WXML 可创建出页面的结构。

  • wxs 不依赖于运行时的基础库版本,可以在所有版本的小程序中运行
  • wxs 与 js 是不同的语言,有自己的语法,并不和 js 一致。
  • wxs 的运行环境和 js 代码是隔离的, wxs 中不能调用其它 js 文件中定义的函数,也不能调用小程序提供的 API。
  • wxs 函数不能作为组件的事件回调。

WXS 页面渲染示例:

<!--wxml-->
<wxs module="m1">
var msg = "hello world";

module.exports.message = msg;
</wxs>

<view>  </view>

输出:

hello world

WXS 数据处理示例:

// page.js
Page({
  data: {
    array: [1, 2, 3, 4, 5, 1, 2, 3, 4]
  }
})

<!--wxml-->
<!-- 下面的 getMax 函数,接受一个数组,且返回数组中最大的元素的值 -->
<wxs module="m1">
var getMax = function(array) {
  var max = undefined;
  for (var i = 0; i < array.length; ++i) {
    max = max === undefined ? 
      array[i] : 
      (max >= array[i] ? max : array[i]);
  }
  return max;
}

module.exports.getMax = getMax;
</wxs>

<!-- 调用 wxs 里面的 getMax 函数,参数为 page.js 里面的 array -->
<view>  </view>

页面输出: 5

自定义组件

基础库版本自 1.6.3 起,小程序支持组件化编程。自定义组件在使用时与基础组件相似。

类似页面,一个自定义组件由 json, wxml, wxss, js 4 个文件组件。并且 json 中要定义: { "component": true }

代码示例:

!-- 这是自定义组件的内部WXML结构 -->
<view class="inner">
  
</view>
<slot></slot>
/* 这里的样式只应用于这个自定义组件 */
.inner {
  color: red;
}

注意在组件的 wxss 中不应使用 ID 选择器、属性选择器和标签名选择器。

在自定义组件的 js 中,需使用 Component() 来注册组件,并提供组件的属性定义、内部数据和自定义方法。

组件的属性值和内部数据将被用于组件 wxml 的渲染,其中属性值可由组件外部传入。

例如:

Component({
  properties: {
    // 这里定义了innerText属性,属性值可以在组件使用时指定
    innerText: {
      type: String,
      value: 'default value',
    }
  },
  data: {
    // 这里是一些组件内部数据
    someData: {}
  },
  methods: {
    // 这里是一个自定义方法
    customMethod: function(){}
  }
})

使用自定义组件前,首先要在页面的 json 文件中进行引用声明,将自定义组件的文件路径对应为一个组件标签名,例如:

{
  "usingComponents": {
    "component-tag-name": "path/to/the/custom/component"
  }
}

之后在模板中就要像使用基础组件一样使用了:

<view>
  <!-- 以下是对一个自定义组件的引用 -->
  <component-tag-name inner-text="Some text"></component-tag-name>
</view>

自定义组件 wxml 中的 slot

<slot/> 节点用于承载组件引用时提供的子节点。例如:

<!-- 组件模板 -->
<view class="wrapper">
  <view>这里是组件的内部节点</view>
  <slot></slot>
</view>

<!-- 引用组件的页面模版 -->
<view>
  <component-tag-name>
    <!-- 这部分内容将被放置在组件 <slot> 的位置上 -->
    <view>这里是插入到组件slot中的内容</view>
  </component-tag-name>
</view>

组件 wxml 中默认只能有一个 slot,需要多个时,要在组件 js 中声明启用:

Component({
  options: {
    multipleSlots: true // 在组件定义时的选项中启用多slot支持
  },
  properties: { /* ... */ },
  methods: { /* ... */ }
})

此时多个 slot 用 name 来区分:

<!-- 组件模板 -->
<view class="wrapper">
  <slot name="before"></slot>
  <view>这里是组件的内部细节</view>
  <slot name="after"></slot>
</view>


<!-- 引用组件的页面模版,使用时,用 slot 属性来将节点插入到不同的slot上。 -->
<view>
  <component-tag-name>
    <!-- 这部分内容将被放置在组件 <slot name="before"> 的位置上 -->
    <view slot="before">这里是插入到组件slot name="before"中的内容</view>
    <!-- 这部分内容将被放置在组件 <slot name="after"> 的位置上 -->
    <view slot="after">这里是插入到组件slot name="after"中的内容</view>
  </component-tag-name>
</view>

自定义组件样式

只对组件 wxml 内的节点有效。

自定义组件中不能使用以下的选择器:

#a { } /* 在组件中不能使用 id 选择器 */
[a] { } /* 在组件中不能使用属性选择器*/
button { } /* 在组件中不能使用标签名选择器 */
.a > .b { } /* 除非 .a 是 view 组件节点,否则不一定会生效 */

可以使用 :host 选择器给托管组件定义样式。

组件中可以通过 externalClasses 指定一些外部样式类名,并在组件中使用这些样式名,而使用者在使用时可以为该样式名赋值,从而实现从外部传入样式的效果,例如:

/* 组件 custom-component.js 中声明外部样式类名*/
Component({
  externalClasses: ['my-class']
})

<!-- 组件 custom-component.wxml 模板中使用该外部样式类名 -->
<custom-component class="my-class">这段文本的颜色由组件外的 class 决定</custom-component>
这样,组件的使用者可以指定这个样式类对应的 class ,就像使用普通属性一样。


<!-- 使用者的 WXML 中为外部样式赋值,并在样式文件中定义具体样式-->
<custom-component my-class="red-text" />


.red-text {
  color: red;
}

Component 构造器

用于定义自定义组件,可以定义组件的属性,数据和方法等。

  • properties: Object Map, 组件的对象属性,是属性名到属性设置的映射表,属性设置中可包含三个字段, type 表示属性类型,value 表示属性初始值,observor 表示属性值被修改时的响应函数
  • data: Object,组件的内部数据,和 properties 一起用于组件的模板渲染
  • methods: Object, 组件的方法,包括事件响应函数和任意的自定义方法。
  • hehaviors: String Arrary, 类似于 mixins 和 traits 的组件间代码利用机制
  • created: Function,组件生命周期函数,在组件实例进入页面节点树时执行,注意此时不能调用 setData
  • attached: Function,组件生命周期函数,在组件实例关联页面节点树时执行
  • ready: Function,组件生命周期函数,在组件布局完成后执行,此时可用 SelectorQuery 获取节点信息
  • moved: Function,组件生命周期函数,在组件实例从页面节点树删除时执行
  • detached: Function,组件生命周期函数,在组件实例进入页面节点树时执行,注意此时不能调用 setData
  • relations: Object, 组件间关系定义
  • externalClasses: String Array, 组件接受的外部样式类名
  • options: Object Map,一些组件选项

生成的组件实例可以在组件的方法、生命周期函数和属性的 observer 中通过 this 访问。

组件有如下一些通用的属性和方法:

  • is 属性: String,组件的文件路径
  • id 属性: String,节点 id
  • dataset 属性: String,节点 dataset
  • data 属性: Object,组件数据,包括内部数据和属性值
  • setData 方法:设置 data 并执行视图层渲染
  • hasBehavior 方法:检查组件是否有该 behavior
  • triggerEvent 方法:触发事件
  • createSelectorQuery 方法:创建一个 SelectorQuery 对象,选择器选取范围为这个组件实例内
  • selectComponent: 使用选择器选择组件实例节点,返回匹配的第一个组件实例对象
  • selectAllComponents: 返回一个匹配数组
  • getRelationNodes: 获取所有该关系对应的所有关联节点

示例:

Component({

  behaviors: [],

  properties: {
    myProperty: { // 属性名
      type: String, // 类型(必填),目前接受的类型包括:String, Number, Boolean, Object, Array, null(表示任意类型)
      value: '', // 属性初始值(可选),如果未指定则会根据类型选择一个
      observer: function(newVal, oldVal){} // 属性被改变时执行的函数(可选),也可以写成在methods段中定义的方法名字符串, 如:'_propertyChange'
    },
    myProperty2: String // 简化的定义方式
  },
  data: {}, // 私有数据,可用于模版渲染

  // 生命周期函数,可以为函数,或一个在methods段中定义的方法名
  attached: function(){},
  moved: function(){},
  detached: function(){},

  methods: {
    onMyButtonTap: function(){
      this.setData({
        // 更新属性和数据的方法与更新页面数据的方法类似
      })
    },
    _myPrivateMethod: function(){
      // 内部方法建议以下划线开头
      this.replaceDataOnPath(['A', 0, 'B'], 'myPrivateData') // 这里将 data.A[0].B 设为 'myPrivateData'
      this.applyDataUpdates()
    },
    _propertyChange: function(newVal, oldVal) {

    }
  }

})

注意在 properties 定义段中,属性名用驼峰写法 propertName,而在 wxml 中指定属性值时则对应使用连字符写法 <component-tag-name property-name="attr val" />,应用于数据绑定时采用驼峰写法 attr=""

自定义组件事件

组件通过 triggerEvent 方法触发生成事件,并指定事件名、detail 对象和事件选项,例如:

<!-- 在自定义组件中 -->
<button bindtap="onTap">点击这个按钮将触发“myevent”事件</button>

// 组件 js 中
Component({
  properties: {}
  methods: {
    onTap: function(){
      var myEventDetail = {} // detail对象,提供给事件监听函数
      var myEventOption = { // 触发事件的选项
          /*
          bubbles: true, //事件是否能冒泡
          composed: false, //事件是否能穿越组件边界,为 false 时,事件只能在引用组件的节点树上触发,不进入其它组件内部
          capturePhase: false //事件是否拥有捕获阶段
          */
      }
      this.triggerEvent('myevent', myEventDetail, myEventOption)
    }
  }
})

监听自定义组件事件的方法同监听基础组件事件的一样。

behaviors

用于组件间代码共享,类似 mixins 或 traits。

和 Component 类似,每个 behavior 可包含一组属性、数据、生命周期函数和方法,组件引用它时,它的属性、数据和方法会被合并到组件中,而生命周期函数也会在对应时机被调用。

每个组件可引用多个 behavior,同时每个 behavior 也可引用其它 behavior。

示例:

// my-behavior.js, behavior 需要使用 Behavior() 构造器定义。
module.exports = Behavior({
  behaviors: [],
  properties: {
    myBehaviorProperty: {
      type: String
    }
  },
  data: {
    myBehaviorData: {}
  },
  attached: function(){},
  methods: {
    myBehaviorMethod: function(){}
  }
})


// my-component.js, 组件引用时,在 behaviors 定义段中将它们逐个列出即可。
var myBehavior = require('my-behavior')
Component({
  behaviors: [myBehavior],
  properties: {
    myProperty: {
      type: String
    }
  },
  data: {
    myData: {}
  },
  attached: function(){},
  methods: {
    myMethod: function(){}
  }
})

字段的覆盖和组件规则

  • 如果有同名的属性或方法,组件本身的属性或方法会覆盖 behavior 中的属性或方法,如果引用了多个 behavior ,在定义段中靠后 behavior 中的属性或方法会覆盖靠前的属性或方法;
  • 如果有同名的数据字段,如果数据是对象类型,会进行对象合并,如果是非对象类型则会进行相互覆盖;
  • 生命周期函数不会相互覆盖,而是在对应触发时机被逐个调用。如果同一个 behavior 被一个组件多次引用,它定义的生命周期函数只会被执行一次。

内置 behaviors

wx://form-field 使组件有类似表单控件的行为,使用:

Component({
  behaviors: ['wx://form-field']
})

组件间的关系

定义自定义父子组件间 linked, linkChanged, unlinked 时的回调动作,例如:

<!-- 组件间的关系 -->
<custom-ul>
  <custom-li> item 1 </custom-li>
  <custom-li> item 2 </custom-li>
</custom-ul>

// path/to/custom-ul.js,自定义组件中定义关系
Component({
  relations: {
    './custom-li': {
      type: 'child', // 关联的目标节点应为子节点
      linked: function(target) {
        // 每次有custom-li被插入时执行,target是该节点实例对象,触发在该节点attached生命周期之后
      },
      linkChanged: function(target) {
        // 每次有custom-li被移动后执行,target是该节点实例对象,触发在该节点moved生命周期之后
      },
      unlinked: function(target) {
        // 每次有custom-li被移除时执行,target是该节点实例对象,触发在该节点detached生命周期之后
      }
    }
  },
  methods: {
    _getAllLi: function(){
      // 使用getRelationNodes可以获得nodes数组,包含所有已关联的custom-li,且是有序的
      var nodes = this.getRelationNodes('path/to/custom-li')
    }
  },
  ready: function(){
    this._getAllLi()
  }
})


// path/to/custom-li.js,自定义组件中定义关系
Component({
  relations: {
    './custom-ul': {
      type: 'parent', // 关联的目标节点应为父节点
      linked: function(target) {
        // 每次被插入到custom-ul时执行,target是custom-ul节点实例对象,触发在attached生命周期之后
      },
      linkChanged: function(target) {
        // 每次被移动后执行,target是custom-ul节点实例对象,触发在moved生命周期之后
      },
      unlinked: function(target) {
        // 每次被移除时执行,target是custom-ul节点实例对象,触发在detached生命周期之后
      }
    }
  }
})

注意:必须在两个组件定义中都加入relations定义,否则不会生效。

抽象节点

自定义组件中声明并使用一个抽象节点(相当于一个节点变量,可在使用该组件中具体赋值),例如:

//自定义组件的 js
Component({
  "usingComponents": {
    "custom-radio": "path/to/custom/radio",
    "custom-checkbox": "path/to/custom/checkbox"
  },
  "componentGenerics": {
    "selectable": { //声明一个抽象节点
      "default": "path/to/default/component" //一个默认值
    }
  }
})

<!-- 自定义组件模板中使用抽象节点 selectable -->
<view wx:for="">
  <label>
    <selectable disabled="false"></selectable>
    
  </label>
</view>


<!-- 在使用该自定义组件时,指定抽象节点的具体值-->
<selectable-group generic:selectable="custom-radio" />
<selectable-group generic:selectable="custom-checkbox" />

多线程 Worker

一些异步处理任务,可放置在 Worker 中运行,再将运行结果返回到小程序的主线程。Worker 运行于一个单独的全局上下文与线程中,不能直接调用主线程的方法。主线程使用 Worker.postMessage() 发送数据,Worker 使用 Worker.onMessage() 接收数据,数据不是直接共享,而是被复制。

步骤为:

先在 app.json 中配置 Worker 代码放置的目录,目录下的代码将打包成一个文件:

{
  "workers": "workers"
}

目录下有文件:

workers/request/index.js
workers/request/utils.js
workers/response/index.js

编写 Worker 代码:

//在 workers/request/index.js 编写 Worker 响应代码

var utils = require('./utils')

// 在 Worker 线程执行上下文会全局暴露一个 `worker` 对象,直接调用 worker.onMeesage/postMessage 即可
worker.onMessage(function (res) {
  console.log(res)
})

在主线程中初始化 Worker,并发送消息

//在主线程的代码 app.js 中初始化 Worker
var worker = wx.createWorker('workers/request/index.js') // 文件名指定 worker 的入口文件路径,绝对路径

//主线程向 Worker 发送消息
worker.postMessage({
  msg: 'hello worker'
})
  • Worker 最大并发数限制为 1 个,创建下一个前调用 Worker.terminate() 结束当前 Worker
  • Worker 内不支持 wx 系列的 API

参考

]]>
小程序基础 2018-04-02T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/miniprogram-basic 代码构成
  1. .json 配置文件
  2. .wxml 页面模板文件,相当于 HTML
  3. .wxss 页面样式文件,相当于 CSS
  4. .js 脚本文件

JSON 配置

app.json

根目录下的 app.json 文件用来保存小程序的全局配置信息,如页面路径信息、窗体 UI 配置、网络超时时间、底部 tab 等。例如:

{
  "pages": [
    "pages/index/index",
    "pages/logs/logs"
  ],
  "window": {
    "navigationBarTitleText": "Demo"
  },
  "tabBar": {
    "list": [{
      "pagePath": "pages/index/index",
      "text": "首页"
    }, {
      "pagePath": "pages/logs/logs",
      "text": "日志"
    }]
  },
  "networkTimeout": {
    "request": 10000,
    "downloadFile": 10000
  },
  "debug": true
}

pages 中定义的第一项将是小程序的默认首页。

project.config.json

根目录下的 project.config.json 保存项目及开发工具的配置信息。

每个页面都可以有自己的配置文件

例如 pages/logs/logs 页面对应的配置文件为 pages/logs/logs.json

WXML 模板

每个页面有一个模板文件,例如 pages/index/index 页对应模板文件 pages/index/index.wxml,例如:

<view class="container">
  <view class="userinfo">
    <button wx:if=""> 获取头像昵称 </button>
    <block wx:else>
      <image src="" background-size="cover"></image>
      <text class="userinfo-nickname"></text>
    </block>
  </view>
  <view class="usermotto">
    <text class="user-motto"></text>
  </view>
</view>

WXML 模板的使用模式,总体上和 Angular 类似。

  • 模板中使用封装了的组件,如 view, button, image, text, block 等。
  • 组件支持 wx:if, wx:else, wx:forwx: 开头的属性,用来控制组件的呈现。
  • 支持使用 `` 表达式将变量值呈现在页面中。

WXSS 样式

具有 CSS 大部分功能。

  • 新增了尺寸单位 rpx (responsive pixel),可以根据屏幕宽度进行自适应,规定所有屏幕宽为 750rpx。
  • 全局样式放在根目录下的 app.wxss 中,每个页面的修改化样式放在各自对应的 page.wxss 中。
  • 仅支持部分 CSS 选择器。

JS 交互逻辑

在模板中为组件添加事件响应绑定,例如:

<button bindtap="clickMe">点击我</button>

在页面对应的 page.js 中定义方法:

Page({
  clickMe: function() {
    this.setData({ msg: "Hello World" })
  }
})

从而当点击 button 时,调用 clickMe 方法。其数据的绑定也类似 React,即每个 Page 对象中都有 data,data 中的数据会更新到模板中,通过 JS 的 Page.setData() 更新 data。

小程序的启动

  1. 打开前,会把整个小程序的代码包下载到本地。
  2. 紧接着将 app.json 文件中的 pages 中的第一项作为首页地址。
  3. 加载首页的代码,通过小程序的一些机制,渲染该页面。

小程序启动后,在 app.js 定义的 App 实例(整个小程序只有一个该实例,全部页面共享)的 onLaunch 回调会被执行:

App({
  onLaunch: function () {
    // 小程序启动之后 触发
  }
})

页面

每个页面都包含有 4 种文件,例如 pages/logs/logs 页面有文件 logs.json, logs.wxml, logs.wxss, logs.js

  1. 微信客户端会先根据 logs.json 配置生成一个界面、颜色、文字等信息。
  2. 加载页面的 WXML 结构和 WXSS 样式。
  3. 最后加载 logs.js,内容例如:
Page({
  data: { // 参与页面渲染的数据
    logs: []
  },
  onLoad: function () {
    // 页面渲染后 执行
  }
})

Page 是一个页面构造器,它生成一个页面实例。在生成页面时,小程序框架会将 Page.data 中的数据和 wxml 模板结构结合起来,最终呈现出页面。

在页面渲染完成后,页面实例会执行 Page.onLoad 回调。

参考

]]>
数据结构 C++ 版本笔记--5.二叉树 2018-03-30T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/dsacpp-binary_tree 五、二叉树

任何有根有序的多叉树,都可等价地转化并实现为二叉树。

btree.png

  • 节点 v 的深度 depth(v): v 到根的通路上所经过的边的数目,也就是 v 所在的层。规定根在第 0 层
  • 节点 v 的度数或(出)度 deg(v): 即 v 的孩子总数。该值也表示该节点连出去的边数
  • n 个节点的树中,其边的总数为所有节点的出度(即 deg9v) 的总和,刚好为 n-1,O(n)
  • 树的所有节点深度的最大值称为树的高度 height(T),约定只有一个节点(即根节点)的树的树高为 0,空树的高为 -1
  • 而节点 v 的高度即为其子树的高度 height(v)
  • 二叉树 binary tree: 即每个节点的度数不超过 2
  • K 叉树 k-ary tree: 每个节点的度数不超过 k 的有根树
  • 在二叉树中,在深度为 k 的层次上,最多有 $2^k$ 个节点
  • 在二叉树中,k 层最多 $2^k$ 个节点,则当有 h 层时,最多有节点数 n= $\sum_{k=0} ^{h}2^k$ = 2^{h+1} -1 < $2^{h+1}$,特别地,当 n= $2^{h+1} -1$,称为满树

btree_numbers.png

多叉树的表示方法

  • 以父节点表示,每个节点中存储父节点信息。访问父节点 O(1),访问子节点,要遍历所有节点,O(n)。

parent_representation.png

  • 以子节点表示,每个节点中将其所有子节点组织为一个列表或向量。若有 r 个子节点,则访问子节点 O(r+1),访问父节点 O(n)。

children_representation.png

  • 父节点+子节点表示,操作方便,但节点的添加删除时,树拓扑结构的维护成本高。

parent_children_representation.png

有序多叉树可转换为二叉树

有序多叉树中,同一节点的所有子节点也定义有次序。

转换时后,原节点的长子(即第一个子节点) 成为了其左节点,原节点的下一个兄弟 成为了其右节点。

mTree2bTree.png

编码树

编码即将一个字符集中的每一个字符映射到一个唯一的二进制串。为避免解码时产生歧义,编码方案中每个字符对应的编码都不能是某个字符编码的前缀。这是一种可行的编码方案,叫 前缀无歧义编码, Prefix-Free Code,PFC

二叉编码树

任一编码方案都可描述为一棵二叉树,每次向右(右)对应一个 0(1)。从根节点到每个节点的唯一通路,可以表述为一个二进制串,称为 根通路串 root path string。PFC 编码树的要求是,每个要映射的字符都必须位于叶子节点,否则,会出现某个字符是另一个字符的父节点,即其编码将会是另一编码的前缀。

如下图中,左边的是一个可行的 PFC 树,右边的不可行。

PFC_instance.png

基于 PFC 编码树的解码算法,可以在二进制串的接收过程中实时进行,属于在线算法

先序遍历

preorder_raversal.png

递归版本

template <typename T, typename VST>
void travPre_R(BinNodePosi(T) p, VST& visit) { //二叉树先序遍历算法(递归版本)
    if (!p)
        return;

    visit(p->data);
    travPre_R(p->lChild, visit);
    travPre_R(p->rChild, visit);
}

消除尾递归

这是一个尾递归,引用辅助栈后可消除尾递归:

//习题 5-10 使用栈消除尾递归
template <typename T, typename VST>
void travPre_I1(BinNodePosi(T) p, VST& visit) { //二叉树先序遍历算法(迭代1:使用栈消除尾递归)
    Stack<BinNodePosi(T)> s; //辅助栈

    if (p) //根节点入栈
        s.push(p);

    while (!s.empty()) { //在栈变空之前反复循环
        p = s.pop();
        visit(p->data); //先访问

        if (HasRChild(*p))
            s.push(p->rChild); //要先压入右子树节点

        if (HasLChild(*p))
            s.push(p->lChild);
    }
}

迭代版本 2

travPre_Iterate.png

考查先序遍历,它的过程,可分解为两段:

  • 先沿 最左侧通路(leftmmost path) 自顶而下访问沿途节点。
  • 再自底而上遍历对应的右子树。

在自顶而下过程中,引用辅助栈,存储对应节点的右子树。

//从当前节点出发,自顶而下沿左分支不断深入,直到没有左分支的节点,沿途节点遇到后立即访问,
//引入辅助栈,存储对应节点的右子树
template <typename T, typename VST>
static void visitAlongLeftBranch(BinNodePosi(T) p, VST& visit, Stack<BinNodePosi(T)>& S){
    while (p) {
        visit(p->data); //访问当前节点

        if (p->rChild) //右孩子入栈暂存(优化:通过判断,避免空的右孩子入栈)
            S.push(p->rChild);
        
        p = p->lChild; //沿左分支深入一层
    }
}

template <typename T, typename VST>
void travPre_I2(BinNodePosi(T) p, VST& visit) { //二叉树先序遍历算法(迭代2)
    Stack<BinNodePosi(T)> S; //辅助栈

    S.push(p);

    while(!S.empty())
        visitAlongLeftBranch( S.pop(), visit, S);
}
//习题 5-23, 在 O(n) 时间内将二叉树中每一节点的左右孩子(其中之一可能为空)互换
// 参考先序遍历的迭代版本
template <typename T>
void swap_pre_R(BinNodePosi(T) p) { //递归版本

    if (p)
        swap(p->lChild, p->rChild);

    if (p->lChild)
        swap_pre_R(p->lChild);

    if (p->rChild)
        swap_pre_R(p->rChild);
}

template <typename T>
void swap_pre_I1(BinNodePosi(T) p) { //使用栈消除尾递归
    Stack<BinNodePosi(T)> S;

    if (p)
        S.push(p);

    while( !S.empty() ){
        p = S.pop();
        swap(p->lChild, p->rChild);

        if (p->rChild)
            S.push(p->rChild);

        if (p->lChild)
            S.push(p->lChild);
    }
}

template <typename T>
void swapAlongLeftBranch(BinNodePosi(T) p, Stack<BinNodePosi(T)>& S) {

    while (p) {
        swap(p->lChild, p->rChild);

        if (p->rChild)
            S.push(p->rChild);

        p = p->lChild;
    }
}

template <typename T>
void swap_pre_I2(BinNodePosi(T) p) {
    Stack<BinNodePosi(T)> S;

    S.push(p);

    while( !S.empty() ) {
        p = S.pop();
        swapAlongLeftBranch(p, S);
    }
}

中序遍历

inorder_traverse.png

递归版本

template <typename T, typename VST>
void travIn_R(BinNodePosi(T) p, VST& visit) { //二叉树中序遍历算法(递归版本)
    if (!p)
        return;

    travIn_R(p->lChild, visit);
    visit(p->data);
    travIn_R(p->rChild, visit);
}

迭代版本 1

inorder_trav_interate.png

考查遍历过程:

  1. 从根结点开始,先沿最左侧通路自顶而下,到达最左的节点(即没有左孩子的节点),将沿途的节点压入辅助栈
  2. 现在可以访问最左的节点了,因此从栈中弹出该节点,访问它,如果它有右孩子,则将右孩子压入栈中(此后在迭代中能完成该右孩子为根的子树的相同遍历过程)
  3. 从栈中弹出一个节点,再次迭代。
template <typename T> //从当前节点出发,沿左分支不断深入,直到没有左分支的节点
static void goAlongLeftBranch(BinNodePosi(T) p, Stack<BinNodePosi(T)>& S) {
    while (p){
        S.push(p);
        p = p->lChild;
    }
}

template <typename T, typename VST>
void travIn_I1(BinNodePosi(T) p, VST& visit) { //二叉树中序遍历算法,迭代版本 1
    Stack<BinNodePosi(T)> S; //辅助栈

    while( true ){
        goAlongLeftBranch(p, S); //从当前节点出发,逐批入栈
        if (S.empty()) //直到所有节点处理完毕
            break;
        p = S.pop(); visit(p->data); //弹出栈顶节点并访问
        p = p->rChild; //转向右子树
    }
}

迭代版本 2


template <typename T, typename VST>
void travIn_I2(BinNodePosi(T) p, VST& visit) { //二叉树中序遍历算法,迭代版本 2
    Stack<BinNodePosi(T)> S; //辅助栈

    while( true ){
        if (p) { //沿最左侧通路自顶而下,将节点压入栈
            S.push(p);
            p = p->lChild;
        }
        else if (!S.empty()) {
            p = S.pop(); //尚未访问的最低祖先节点
            visit(p->data);
            p = p->rChild; //遍历该节点的右子树
        }
        else 
            break;  //遍历完成
    }
}

直接后继及其定位

遍历能将半线性的二叉树转化为线性结构。于是指定遍历策略后,就能在节点间定义前驱和后继了。其中没有前驱(后继)的节点称作首(末)节点。

定位中序遍历中的直接后继对二叉搜索树很重要。

template <typename T>
BinNodePosi(T) BinNode<T>::succ() { //定位节点 v 的直接后继
    BinNodePosi(T) s = this; //记录后继的临时变量

    if (rChild) { //若有右孩子,则直接后继必在右子树中,具体地就是
        s = rChild; //右子树中的
        while (HasLChild(*s)) //最靠左(最小)的节点
            s = s->lChild;
    } else { //否则,直接后继应是 “将当前节点包含于基左子树中的最低祖先”,具体地就是
        while (IsRChild(*s))
            s = s->parent; //逆向地沿右向分支,不断朝左上方移动

        s = s->parent; //最后再朝右上方移动一步,即抵达直接后继(如果存在)
    }

    return s;
}

有右孩子的情况,如下图中的节点 b, 直接后继就是右子树中的最左节点 c。

没有右孩子的情况,如图中的 e, 查找过程是先沿右向分支不断朝左上方移到 d,最后再朝右上方移动一步到 f,即后继为 f,特别地,节点 g 的后继为 NULL。

inorder_I_instance.png

succ_inorder.png

//习题 5-14
//遍历能将半线性的二叉树转化为线性结构。于是指定遍历策略后,就能在节点间定义前驱和后继了。其中没有前驱(后继)的节点称作首(末)节点。
template <typename T>
BinNodePosi(T) BinNode<T>::pred() { //定位节点 v 的直接前驱
    BinNodePosi(T) s = this; //记录前驱的临时变量

    if (lChild) { //若有左孩子,则直接前继必在左子树中,具体地就是
        s = lChild; //左子树中的
        while (HasRChild(*s)) //最靠右(最大)的节点
            s = s->rChild;
    } else { //否则,直接前继应是 “将当前节点包含于其左子树中的最低祖先”,具体地就是
        while (IsLChild(*s))
            s = s->parent; //逆向地沿左向分支,不断朝右上方移动

        s = s->parent; //最后再朝左上方移动一步,即抵达直接前驱(如果存在)
    }

    return s;
}

迭代版本 3

template <typename T, typename VST>
void travIn_I3(BinNodePosi(T) p, VST& visit) { //二叉树中序遍历算法:版本 3, 无需辅助栈
    bool backtrack = false; //前一步是否刚从右子树回溯 -- 省去栈,仅 O(1) 辅助空间
                            //回溯回来的表示当前节点的左侧都已经访问过了

    while (true)
        if (!backtrack && HasLChild(*p)) //若有左子树且不是刚刚回溯,则
            p = p->lChild; //深入遍历左子树
        else { //否则--无左子树或刚刚回溯(左子树已访问完毕)
            visit(p->data); //访问该节点
            if (HasRChild(*p)) { //若有右子树,则
                p = p->rChild; //深入右子树继续遍历
                backtrack = false; //并关闭回溯标志
            } else { // 若右子树为空,则
                if (!(p=p->succ())) //后继为空,表示抵达了末节点
                    break;
                backtrack = true; //并设置回溯标志
            }
        }
}

inorder_backtrack.png

迭代版本 4

//习题 5-17
template <typename T, typename VST>
void travIn_I4(BinNodePosi(T) p, VST& visit) { //二叉树中序遍历算法:版本4,无需辅助栈和标记
    while (true) {
        if (HasLChild(*p))  //若有左子树,则
            p = p->lChild;  //深入遍历左子树
        else {              //否则
            visit(p->data);  //访问当前节点,并
            while (!HasRChild(*p))   //不断地在无右分支处
                if (!(p=p->succ()))  //回溯至直接后继(在没有后继的末节点处,直接退出)
                    return;
                else
                    visit(p->data);  //访问新的当前节点
            p = p->rChild;           //直到有右分支处,转向非空的右子树
        }
    }
}

后序遍历

postorder_traverse.png

递归版本

template <typename T, typename VST>
void travPost_R(BinNodePosi(T) p, VST& visit) { //二叉树后序遍历算法(递归版本)
    if (!p)
        return;

    travPost_R(p->lChild, visit);
    travPost_R(p->rChild, visit);
    visit(p->data);
}

迭代版本

postorder_iterate.png

将树 T 画在二维平面上,从左侧水平向右看去,未被遮挡的最高叶节点 v(称作最高左侧可见叶节点 HLVFL),即为后序遍历首先访问的节点,该节点可能是左孩子,也可能是右孩子(故用垂直边表示)。

沿着v 与树根之间的通路自底而上,整个遍历也可分解为若干个片段。每一片段,分别起始于通路上的一个节点,并包括三步:访问当前节点,遍历以其右兄弟(若存在)为根的子树,最后向上回溯至其父节点(若存在)并转下下一片段。

在此过程中,依然利用栈逆序地保存沿途所经各节点,以确定遍历序列各个片段在宏观上的拼接次序。

template <typename T> //在以栈 S 顶节点为根的子树中,找到最高左侧可见叶节点
static void gotoHLVFL(Stack<BinNodePosi(T)> & S) { //沿途所遇节点依次入栈
    while (BinNodePosi(T) p = S.top()) //自顶而下,反复检查当前节点(即栈顶)
        if (HasLChild(*p)) { //尽可能向左
            if (HasRChild(*p))
                S.push(p->rChild); //若有右孩子,优先入栈
            S.push(p->lChild); //然后才转至左孩子
        }
        else //实不得已
            S.push(p->rChild); //才向右

    S.pop();//返回之前,弹出栈顶的空节点
}

template <typename T, typename VST>
void travPost_I(BinNodePosi(T) p, VST& visit) { //二叉树的后序遍历(迭代版本)
    Stack<BinNodePosi(T)> S; //辅助栈

    if (p)
        S.push(p); //根入栈

    while (!S.empty()) {
        if (S.top() != p->parent) //若栈顶不是当前节点之父(则必为其右兄),
            gotoHLVFL(S); //则此时以其右兄为根的子树中,找到 HLVFL

        p = S.pop(); //弹出该前一节点之后继,并访问
        visit(p->data); 
    }
}
//习题 5-25
// O(n) 内将每个节点的数值替换为其后代中的最大数值
// 参考后序遍历
#define MIN_T 0  //设 T 类型的最小值为 0
template <typename T>
T replace_as_children_largest_post_R(BinNodePosi(T) p) { //参考后序递归版本
    if (!p)
        return MIN_T;

    T max_left = replace_as_children_largest_post_R(p->lChild);
    T max_right = replace_as_children_largest_post_R(p->rChild);
    p->data = max( p->data, max( max_left, max_right));
    return p->data;
}

template <typename T>
void replace_as_children_largest_post_I(BinNodePosi(T) p) { //参考后序迭代版本
    Stack<BinNodePosi(T)> S;

    if (p)
        S.push(p);

    while(!S.empty()) {
        if (S.top() != p->parent) //若栈顶不是当前节点之父,则必为其右兄
            gotoHLVFL(S); //则此时以其兄为根的子树中,找到 HLVFL

        p = S.pop();

        if (p->lChild && p->data < p->lChild->data)
            p->data = p->lChild->data;
        if (p->rChild && p->data < p->rChild->data)
            p->data = p->rChild->data;

    }
}

层次遍历

即先上后下,先左后右,借助队列实现。

levelorder_traversal.png

// 层次遍历
//即先上后下,先左后右,借助队列实现。
template <typename T, typename VST>
void travLevel(BinNodePosi(T) p, VST& visit) { //二叉树层次遍历
    Queue<BinNodePosi(T)> Q; //辅助队列
    Q.enqueue(p); //根入队

    while (!Q.empty()) {
        BinNodePosi(T) p = Q.dequeue(); visit(p->data); //取出队首节点并访问

        if (HasLChild(*p))
                Q.enqueue(p->lChild);

        if (HasRChild(*p))
                Q.enqueue(p->rChild);
    }
}

完全二叉树 complete binary tree

叶节点只能出现在最底部的两层,且最底层叶节点均处于次底层叶节点的左侧。

complete_btree.png

对于高度为 h 的完全二叉树,规模应在 $2^h$ 和 $2^{h+1} - 1$ 之间。

完全二叉树 可借助向量结构实现紧凑存储和高效访问。

满二叉树 full binary tree

每层结点都饱和。

full_btree.png

第 k 层的节点数是 $2^k$,当高为 h 时,总结点数是 $2^0+ 2^1+\cdots+2^h = 2^{h+1}-1$,内部节点是 $2^{h+1}-1 -2^h = 2^h-1$,叶结点为 $2^h$,叶节点总是恰好比内部节点数多 1。

二叉树的重构

中序 + 先序(或后序)就能还原二叉树。

btree_rebuild.png

以先序+中序为例,用数学归纳法,设当 n<N 时以上结论成立。当 n=N 时,由先序遍历序列可知,这一节点为根节点。再由中序序列,可得出左子树和右子树,从而问题规模减少为两个子树,和假设相符,故成立。

只用先序+后序无法还原,因为当某个子树为空时会有岐义,但是当任何节点的子树个数为偶数时(0或2时,即为真二叉树)可还原。

Huffman 编码

PFC 编解码

PFC_progress.png

可自底而上,两两子集合并,最终生成一个全集。首先,由每一字符分别构造一棵单节点二叉树,并将它们视作一个森林(向量),此后,反复从森林中取出两棵树并将其合二为一,从而合并生成一棵完整的 PFC 编码树。再将 PFC 编码树转译成编码表。之后通过查表,将字符转化成对应的二进制编码串。

解码过程为,将接收到的编码串在编码树中反复从根节点出发做相应的漫游,依次完成各字符的解码。

最优编码树

编码效率主要体现在所生成的二进制编码串的平均长度。

字符 x 的编码长度即为其对应叶节点的深度 depth(v(x)),而各字符的平均编码长度即为编码树 T 中各叶子节点的平均深度 (average leaf depth, ald)。

ald(T) 值最小时,对应一棵最优编码树。

最优编码树的性质:

  • 双子性:即该树必是真二叉树,其内部节点的左右子全双。
  • 层次性:任何叶节点间的深度差不得超过 1。

因此,其叶节点只能出现于最低两层,这类树的一种特例就是真完全树。

构建方法

若字符集含 n 个字符,则先创建一棵规模为 2n-1 的完全二叉树,将字符集中的字符任意分配给 T 的 n 个叶节点即可。

Huffman 编码树

考虑字符出现的概率不同,考查带权平均编码长度与叶节点的带权平均深度 wald(T)。

这种情况下,完全二叉编码树或满树,其对应的 wald(T) 不一定是最小的。

最优带权编码树

其 wald(T) 最小。

性质:

  • 双子性依然满足
  • 层次性: 若字符 x 和 y 出现的概率最低,则它们必同处于最优树的最底层,且互为兄弟。

Huffman 编码算法

已知字符集以及各字符出现的概率。

1、选出概率最小的两个字符,根据层次性,这两节点必处于最底层,将这两节点合并成一棵二叉树,树根节点的概率为该两字符概率之和。 2、将以上构建的子树的根假想为一个字符,返回原来的字符集中一并处理,返回第 1 步再反复合并。最后可得到一棵最优带权编码树(即 Huffman 编码树)。

Huffman 编码树只是最优带权编码树中的一棵。

参考

]]>
数据结构 C++ 版本笔记--4.栈与队列 2018-03-21T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/dsacpp-stack_queue 4、栈与队列

stack.png

栈与递归

函数调用和递归调用都通过栈完成。调用栈中的各帧,依次对应一个尚未返回的调用实例(即当前的活跃函数实例 active function instance),即每次调用时,都相应创建帧,记录调用实例在二进制程序中的返回地址,局部变量、传入参数等信息,并将该帧压入调用栈。

栈的应用

逆序输出

进制转换

从 10 进制转为 $\lambda$ 进制,如 $12345{(10)} = 30071{(8)}$

//栈的应用
// 逆序输出 1. 进制转换
void convert(Stack<char>& s, int n, int base) { //十进制数 n 到 base 进制的转换
    static char digit[] //0<n, 1<base<=16, 新进制下的数位符号,可视base取值适当扩充
        = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };

    while (n>0) { //由低到高,逐一计算出新进制下的各数位
        int remainder = (int) n % base; //余数即为当前位
        s.push(digit[remainder]);
        n /= base;
    }
}//新进制下由高到低的各数位,自顶而下保存于栈中

void convert_number(int n, int base) {
    Stack<char> s = Stack<char>();

    convert(s, 12345, 8);
    cout << "12345(10) --> ";
    while( !s.empty() ){
        cout << s.pop();
    }
    cout << "(8)" << endl;
}

convert_number(12345, 8); //30071

栈混洗

考查三个栈 A, B 和 S。其中 B 和 S 为空,A 含有 n 个元素,自顶向下构成输入序列 A = <a1, a2, ..., an]

只允许 2 种操作:

  • 将 A 中的元素 pop 出随即 push 到 S
  • 将 S 中的元素 pop 出随即 push 到 B

则经 n 次这样的操作后,A 中的元素全部 B 中,而 B 中的元素序列即为原输入序列的一个栈混洗 stack permutation。

下面是一个栈混洗实例,上左为 A(输入),上右为 B(输出),下面为 S(临时栈):

stack_permutation_instance.png

可看出,对于长度为 n 的输入序列,每一个栈混洗对应于由 n 次 push 和 n 次 pop 构成的某一个合法操序列,比如上面的 [3, 2, 4, 1] 即对应于 {push, push, push, pop, pop, push, pop, pop }。反之,由 n 次 push 和 n 次 pop 构成的任何操作序列,只要满足 任一前缀中的 push 不少于 pop 这一限制,则该序列也必然对应于某个栈混洗。

栈混洗数目

设长度为 n 的输入序列,共有 SP(n) 个栈混洗,则 SP(1) = 1, 考查元素从 S 中 pop 出随即 push 入 B 时,此时设 B 中已经有 k-1 个元素,那么 A 中还有 n-k 个元素,此时的 栈混洗数为 $SP(k-1) \cdot SP(n-k)$, 根据 k 的可能取值,则

值恰好是著名的 catalan 数,为 $catalan(n) = (2n)!/((n+1)! \cdot n!)$

stack_permutation_count.png

甄别是否合法栈混洗

当 n=3 时, <1,2,3] 共有 6!/(4!*3!) = 5 种合法栈混洗,而其全排序为 3!=6 种,只有 B = [3, 1, 2> 是非法的。

对于输入序列中有 1<=i<j<k<=n 时,如果输出中有 [..., k,... i, ....,j,...>,即必非栈混洗。

进一步地,对于 i<j,输出中必不含 模式 [..., j+1, ..., i,..., j...>

充要性: A permutation is a stack permutation, it does NOT involve the permutation 312。(Kunth, 1968)

stack_permutation_check.png

括号匹配

合法的栈混洗中,满足 任一前缀中的 push 不少于 pop 这一限制。因此一个合法的栈混洗,刚好对应于一种括号匹配。如上面的 {push, push, push, pop, pop, push, pop, pop } 对应 ((())())

//栈的应用 2. 括号匹配
bool parentheses_match(const char exp[], int lo, int hi) { //表达式括号匹配检查,可兼顾三种括号
    Stack<char> s = Stack<char>(); //使用栈记录已发现但尚未匹配的括号
    for( int i=lo; i<hi; i++) {
        switch( exp[i] ){
            case '(': case '[': case '{':  //左括号直接进栈
                s.push(exp[i]); 
                break;
            case ')':  //右括号若与栈顶失配,则表达式必不匹配
                if (s.empty() || s.pop() != '(') {
                    return false;
                }
                break;
            case ']':
                if (s.empty() || s.pop() != '[') {
                    return false;
                }
                break;
            case '}':
                if (s.empty() || s.pop() != '{') {
                    return false;
                }
                break;
        default: //非括号字符一律忽略
                break;
        }
    }
    return s.empty();
}



char* parentheses_str1 = "a / ( b [ i - 1 ] [ j + 1 ] + c [ i + 1 ] [ j - 1 ] ) * 2";
char* parentheses_str2 = "a / ( b [ i - 1 ] [ j + 1 ] ) + c [ i + 1 ] [ j - 1 ] ) * 2";
cout << parentheses_str1 << " is match: " << parentheses_match(parentheses_str1, 0, 56) << endl;
cout << parentheses_str2 << " is match: " << parentheses_match(parentheses_str2, 0, 58) << endl;

延迟缓冲

如中缀表达式求值时,必须等所有表达式字符全部扫描后,才能进行求值,此时用栈来缓存。

下面的代码中,利用了栈来求值中缀表达式的值,并将中缀表达式转换成了后缀表达式(逆波兰表达式)。

//习题 4-6
// 输出一个浮点数并压入栈中
float readNumber(char* &S, Stack<float> & opnd) {
    opnd.push(0); //初始化为 0
    char c = *S;
    bool is_decimal = false; //是否处理的是小数部分
    float decimal_base = 0.1;
    while ( *S ) {
        if (isdigit(*S)){
            float val = opnd.pop();
            int digit = int(*S)-0x30;

            if (!is_decimal) //处理非小数部分, x = x*10 + y
                opnd.push( val * 10 + digit );
            else { //处理小数部分,x=x+y*decimal_base
                opnd.push( val + digit*decimal_base );
                decimal_base *= 0.1; // 当有小数时,计算下一个数位的基数
            }
            S++;
        }
        else if ( *S == '.' ) {
            is_decimal = true;
            decimal_base = 0.1;
            S++;
        }
        else break;
    }
    return opnd.top();
}

float calcu(char op, float opnd) { //实施一元计算
    if (op == '!'){
        float ret = 1;
        for (int i=(int)opnd; i>1; i--)
            ret *= i;
        return ret;
    }
    return -1;
}

float calcu( float pOpnd1, char op, float pOpnd2 ) {//实施二元计算,结果入栈
    switch(op){
        case '+':
            return pOpnd1 + pOpnd2; break;
        case '-':
            return pOpnd1 - pOpnd2; break;
        case '*':
            return pOpnd1 * pOpnd2; break;
        case '/':
            return pOpnd1 / pOpnd2; break;
        case '^':
            return pow(pOpnd1, pOpnd2); break;
    }
    return -1;
}

void append( string &RPN, char op){ //读入操作数,并将其接至 PRN 末尾
    RPN = RPN + op;
}

void append( string &RPN, float opnd){ //读入操作数,并将其接至 PRN 末尾
    ostringstream ss;
    ss << opnd;
    RPN  = RPN + ss.str();
}

void append( string &RPN, double opnd){ //读入操作数,并将其接至 PRN 末尾
    ostringstream ss;
    ss << opnd;
    RPN  = RPN + ss.str();
}

//栈的应用 3. 中缀表达式求值
#define N_OPTR 9 //运算符总数
typedef enum { ADD, SUB, MUL, DIV, POW, FAC, L_P, R_P, EOE } Operator; //运算符集合
//加,减,乘,除,乘方,阶乘,左括号,右括号,起始符与终止符

const char pri[N_OPTR][N_OPTR] = { //运算符优先等级[栈顶][当前]
    /*          |------------当前运算符-----------| */
    /*          +     -     *     /     ^     !     (     )     \0 */
    /* -- + */ '>',  '>',  '<',  '<',  '<',  '<',  '<',  '>',  '>',    
    /* |  - */ '>',  '>',  '<',  '<',  '<',  '<',  '<',  '>',  '>',    
    /* 栈 * */ '>',  '>',  '>',  '>',  '<',  '<',  '<',  '>',  '>',    
    /* 顶 / */ '>',  '>',  '>',  '>',  '<',  '<',  '<',  '>',  '>',    
    /* 运 ^ */ '>',  '>',  '>',  '>',  '>',  '<',  '<',  '>',  '>',    
    /* 算 ! */ '>',  '>',  '>',  '>',  '>',  '>',  ' ',  '>',  '>',    
    /* 符 ( */ '<',  '<',  '<',  '<',  '<',  '<',  '<',  '=',  ' ',    
    /* |  ) */ ' ',  ' ',  ' ',  ' ',  ' ',  ' ',  ' ',  ' ',  ' ',    
    /* --\0 */ '<',  '<',  '<',  '<',  '<',  '<',  '<',  ' ',  '='    
};

int op2index(char op) {
    switch(op){
        case '+': return 0; break;
        case '-': return 1; break;
        case '*': return 2; break;
        case '/': return 3; break;
        case '^': return 4; break;
        case '!': return 5; break;
        case '(': return 6; break;
        case ')': return 7; break;
        case '\0': return 8; break;
        default: return -1; break;
    }
}

char orderBetween(char op1, char op2) { //视其与栈顶运算符之间优先级高低分别处理
    int index1 = op2index(op1);
    int index2 = op2index(op2);
    return pri[index1][index2];
}

float evaluate( char* S, string & RPN) { //对已剔除空白符的表达式求值,并转换为逆波兰式 RPN
    Stack<float> opnd; //运算数栈
    Stack<char> optr; //运算符栈

    optr.push('\0'); //尾哨兵 '\0' 也作为头哨兵首先入栈

    while (!optr.empty()) { //在运算符栈非空之前,逐个处理表达式中各字符
        if ( isdigit(*S) ) { //若当前字符为操作数,则
            readNumber(S, opnd);
            append( RPN, opnd.top() ); //读入操作数,并将其接至 PRN 末尾
        }
        else {//若当前字符为运算符
            switch( orderBetween(optr.top(), *S) ) { //视其与栈顶运算符之间优先级高低分别处理
                case '<': //栈顶运算符低优先级时
                    optr.push( *S ); S++; //计算推迟,当前运算符进栈
                    break;
                case '=': //优先级相等,(当前运算符为右括号或尾部哨兵\0)时
                    optr.pop(); S++; //脱括号并接收下一个字符
                    break;
                case '>':{ //栈顶运算符高优先级时,可实施相应的计算,并将结果重新入栈
                    char op = optr.pop(); append( RPN, op ); //栈顶运算符出栈并续接至 RPN 末尾
                    if ( '!' == op ) { //! 为一元操作符
                        float pOpnd = opnd.pop();
                        opnd.push( calcu( op, pOpnd ) ); //实施一元计算,结果入栈
                    } else { //其它都为二元操作符
                        float pOpnd2 = opnd.pop(), pOpnd1 = opnd.pop();
                        opnd.push( calcu( pOpnd1, op, pOpnd2 ) ); //实施二元计算,结果入栈
                    }
                    break;
                }
                default:
                    exit(-1); //逢语法错误,不做处理直接退出
                    break;
            } //switch
        }
    }//while

    return opnd.pop(); //弹出并返回最后的计算结果
}


char* exp = "(1+2)*3^4";
string RPN = "";
cout << exp << "=" << evaluate(exp, RPN) << endl; //(1+2)*3^4=243
cout << "RPN is:" << RPN << endl; //RPN is:12+34^*

逆波兰表达式

手工将中缀表达式转换成后缀表达式:

  • 操作数的位置保持不变
  • 运算符的位置,恰好就是其对应的操作数均就绪时的后面。

例如: ( 0 ! + 1 ) * 2 ^ ( 3 ! + 4 ) - ( 5 ! - 67 - ( 8 + 9 ) ),转换成 0!1+ 2 3!4+^* 5! 67- 89+--

试探回溯法

八皇后问题

国际象棋中皇后的热力范围覆盖其所在的水平线、垂直线及两条对角线,考查在 nXn 的棋盘上放置 n 个皇后,如何使得她们互不攻击。

由鸽巢原理知,n 行 n 列棋盘上至多只能放 n 个皇后,反之,n 个皇后在 nXn 棋盘上的可行棋局通常也存在。

8queens.png

算法实现:

基于试探回溯策略。既然每行能且仅能放置一个皇后,故首先将各皇后分配至每一行。然后从空棋盘开始,逐个尝试着将她们放置到无冲突的某列。每设置好一个皇后,再继续试探下一个。若当前皇后在任何列都会造成冲突,则后续皇后的试探都将是徒劳的,故此时应该回溯到上一个皇后进行再试探。

迭代版本中用一个栈来保存皇后的位置,开始为空棋盘,并从原点位置出发开始尝试。当栈中的元素个数等于棋盘行(列)数时,则得到一个解。

//国际象棋中皇后的热力范围覆盖其所在的水平线、垂直线及两条对角线,考查在 nXn 的棋盘上放置 n 个皇后,如何使得她们互不攻击。
//由鸽巢原理知,n 行 n 列棋盘上至多只能放 n 个皇后,反之,n 个皇后在 nXn 棋盘上的可行棋局通常也存在。
struct Queen { //皇后类
    int x, y; //皇后在棋盘上的位置坐标
    Queen(int xx=0, int yy=0): x(xx), y(yy) {};
    bool operator == (Queen const& q) const { //重载判等操作符,以检测不同皇后之间可能的冲突
        return (x == q.x) // 行冲突,这一情况其实并不会发生,可省略
            || (y == q.y)
            || (x+y == q.x+q.y) //右上、左下对角线冲突
            || (x-y == q.x-q.y); // 左上、右下对象线冲突
    }

    bool operator != (Queen const& q) const { return ! (*this == q); }

};

int nSolu = 0; // 保存 N 皇后问题的解的个数
int nCheck = 0; //保存 N 皇后问题求解过程中的尝试次数

//迭代版本中用栈保存位置。
//开始为空棋盘,并从原点位置出发开始尝试
void placeQueens( int N ){ //N 皇后算法迭代版:采用试探/回溯策略,借助栈记录查找的结果
    Stack<Queen> solu; //存放(部分)解的栈
    Queen q(0, 0); //从原点位置出发

    do { //反复试探与回溯
        if ( N <= solu.size() || N <= q.y ) { //若已出界,则
            q = solu.pop(); //回溯一行,并接着试探该行中的下一列
            q.y++; 
        } 
        else { //否则,试探下一行
            while ( (q.y < N) && ( 0 <= solu.find(q)) ) { //通过与已有皇后的对比
                q.y++; //尝试找到可摆放下一皇后的列
                nCheck++;
            }

            if (q.y < N) { //若找到可摆放的列,则
                solu.push(q); //摆上当前皇后,并
                if (N <= solu.size() ) {  //若部分解已成为全局解,则通过全局变量 nSolu 计数
                    nSolu++;
                }

                q.x++; //转入下一行,从第 0 列开始,试探下一皇后
                q.y = 0;
            }
        }
    } while( ( 0<q.x ) || (q.y < N) );
}


placeQueens(8);
cout << "8 Queens solutions = " << nSolu << endl; //92
cout << "8 Queens solution checks = " << nCheck << endl; //13664

4queensInstance.png

费马-拉格朗日分解

Fermat-Lagrange 定理指出,任何一个自然数 n 都可以表示为 4 个整数的平方和,如 $30 = 1^2 + 2^2 + 3^2 + 4^2$。

采用试探回溯策略解, 分解得到的每个自然数 q 都有 $q <= \sqrt n = N$,类似 N 皇后问题,其中行数为 4 行,列数为 N 行(即每个自然数的取值)。

下面的代码中,对于不超过 n 的每一个自然数,给出了其分解的总数(同一组数的不同排序视作等同),同时给出了 n 的所有分解。

//Fermat-Lagrange 定理:任何一个自然数都可以表示为 4 个整数的平方和,
//如 30 = 1^2 + 2^2 + 3^2 + 4^2
//试
//
int nFLSolu = 0; // 保存解的个数
int nFLCheck = 0; //保存求解过程中的尝试次数

void fermat_lagrange( int n, int counts[] ){ //n 分解
    Stack<int> solu; //存放(部分)解的栈
    int q = 0; //从第一个自然数开始,相当于第一行
    int N = (int)sqrt(n); //列数
    int stack_sum = 0; //栈中所有元素的平方和

    do { //反复试探与回溯
        if ( 4 <= solu.size() || N < q ) { //若已出界,则
            q = solu.pop(); //回溯一行,并接着试探该行中的下一列
            stack_sum -= q*q;
            q++; 
        } 
        else { //否则,试探下一行
            if (q <=N && stack_sum + q*q <=n) {
                solu.push(q);
                stack_sum += q*q;

                if (4 == solu.size()){
                    counts[stack_sum] += 1; //统计不超过 n 的每一自然数的分解数

                    if (stack_sum == n){ //局部解是全局解时
                        nSolu ++;
                        solu.report("out");
                    }
                }
                //q = 0; //下一行开头
                //q = q; // 下一行值 >= q,从而能排除同一组数的不同排列

            }
            else { //q 值及以上的都不符合
                q = N+1; //使 q 越界
            }

        }
    } while( ( 0 < solu.size() ) || ( q <= N) );
}

    int counts[101] = { 0 };
    cout << "fermat_lagrange(100):" << endl;
    fermat_lagrange( 100, counts );
    for (int i=0; i<=100; i++){
        cout << "fermat_lagrange " << i << " counts: " << counts[i] << endl;
    }

迷宫寻径

nXn 个方格组成的迷宫,除了四周的围墙,还有分布期间的若干障碍物,只能水平或垂直移动。寻径的任务是:在任意指定的起始格点与目标格点之间,找出一条通路(如果的确存在)。

labyrinth_instance.png

// 迷宫寻径
// nXn 个方格组成的迷宫,除了四周的围墙,还有分布期间的若干障碍物,只能水平或垂直移动。寻径的任务是:在任意指定的起始格点与目标格点之间,找出一条通路(如果的确存在)。

//迷宫格点 Cell
typedef enum { AVAILABLE, ROUTE, BACKTRACED, WALL } Status; //迷宫单元格点状态
// 共 4 种状态: 原始可用,在当前路径上的,所有方向均尝试失败后回溯过的,不可使用的(墙)
// 属于当前路径的格点,还需记录其前驱和后继格点的方向。

typedef enum { UNKNOWN, EAST, SOUTH, WEST, NORTH, NO_WAY } ESWN;  //单元格点的相对邻接方向
// 未定,东,南,西,北,无路可通
// 既然只有上下左右四个连接方向,故 E S W N 可区别。特别地,因尚未搜索到而仍处理初始 AVAILABLE
// 状态的格点,邻格的方向都是 UNKNOWN,经过回溯后处于 BACKTRACED 状态的格点,与邻格间的连接关系
// 均关闭,故标记为 NO_WAY

inline ESWN nextESWN( ESWN eswn ) { return ESWN(eswn+1); } //依次转至下一邻接方向

struct Cell { //迷宫格点
    int x, y; Status status; // 坐标; 状态/类型
    ESWN incoming, outgoing; //进入,走出方向,即其前驱和后续格点的方位
};


#define LABY_MAX 13 //最大迷宫尺寸
Cell laby[LABY_MAX][LABY_MAX]; //迷宫,是一个二维数组

inline Cell* neighbor (Cell* cell) { //查询当前格点的后继格点
    switch( cell->outgoing ){
        case EAST: return cell + 1;         // ------->
        case WEST: return cell - 1;         // <-------
        case SOUTH: return cell + LABY_MAX; //       |
                                            //       V
        case NORTH: return cell - LABY_MAX; //   ^
                                            //   |
        default: exit(-1);
    }
}

inline Cell* advance ( Cell* cell ) { //从当前格点转入相邻格点,并设置前驱格点的方向
    Cell* next;
    switch( cell->outgoing ) {
        case EAST:
            next = cell + 1;
            next->incoming = WEST; break;
        case WEST:
            next =  cell - 1;
            next->incoming = EAST; break;
        case SOUTH: 
            next = cell + LABY_MAX;
            next->incoming = NORTH; break;
        case NORTH:
            next = cell - LABY_MAX;
            next->incoming = SOUTH; break;
        default: exit(-1);
    }
    return next;
}

//实现:借助栈按次序记录组成当前路径的所有格点,
//并动态地随着试探、回溯做入栈、出栈操作。
//路径的起始格点、当前的末端格点分别对应于路径中的
//栈底和栈项,当后者抵达目标格点时探索成功。
// 迷宫寻径算法:在格点 s 至 t 之间规划一条通路(如果的确存在)
bool labyrinth( Cell Laby[LABY_MAX][LABY_MAX], Cell* s, Cell* t, Stack<Cell*> &path) {
    if ( (AVAILABLE != s->status ) || (AVAILABLE != t->status) )
        return false; //退化情况

    //Stack<Cell*> path; //用栈记录通路

    s->incoming = UNKNOWN; //起点
    s->status = ROUTE;
    path.push(s);

    do { //从起点出发不断试探、回溯,直到抵达终点、或穷尽所有可能
        Cell* c = path.top(); //检查当前位置(栈顶)
        if (c == t) //若已抵达终点,则找到了一条通路,否则沿尚未试探的方向继续试探
            return true;

        while ( (c->outgoing = nextESWN(c->outgoing)) < NO_WAY ) //逐一检查所有方向
            if (AVAILABLE == neighbor(c)->status) //直到找到一个未尝试过的方向
                break;

        if ( NO_WAY <= c->outgoing ) { //若所有方向都已尝试过
            c->status = BACKTRACED; //则标记并且回溯
            c = path.pop();
        }
        else { //还有若尝试的,则向前试探一步
            path.push( c=advance(c) );
            c->outgoing = UNKNOWN;
            c->status = ROUTE;
        }
    } while (!path.empty());

    return false;
}

    cout << "test labyrinth:" << endl; //见P104 的 13X13 实例
    for (int i=0; i<LABY_MAX; i++) {
        for ( int j=0; j<LABY_MAX; j++) {
            laby[i][j].x = i;
            laby[i][j].y = j;
            laby[i][j].status = AVAILABLE;
            laby[i][j].incoming = UNKNOWN;
            laby[i][j].outgoing = UNKNOWN;
        }
    }

    for (int i=0; i<LABY_MAX; i++) {
        laby[0][i].status = WALL; //第一行
        laby[LABY_MAX-1][i].status = WALL; //最后一行

        laby[i][0].status = WALL; //第一列
        laby[i][LABY_MAX-1].status = WALL; //最后一列
    }

    laby[1][2].status = WALL;
    laby[1][3].status = WALL;
    laby[1][6].status = WALL;
    laby[2][1].status = WALL;
    laby[2][3].status = WALL;
    laby[2][4].status = WALL;
    laby[2][7].status = WALL;
    laby[2][9].status = WALL;
    laby[3][5].status = WALL;
    laby[3][6].status = WALL;
    laby[3][8].status = WALL;
    laby[4][5].status = WALL;
    laby[5][1].status = WALL;
    laby[5][5].status = WALL;
    laby[6][5].status = WALL;
    laby[7][2].status = WALL;
    laby[7][3].status = WALL;
    laby[7][6].status = WALL;
    laby[7][9].status = WALL;
    laby[8][1].status = WALL;
    laby[8][6].status = WALL;
    laby[9][3].status = WALL;
    laby[9][6].status = WALL;
    laby[9][7].status = WALL;
    laby[9][9].status = WALL;
    laby[10][10].status = WALL;
    laby[11][8].status = WALL;
    laby[11][10].status = WALL;
    laby[11][11].status = WALL;

    Cell* ss = &laby[4][9];
    Cell* tt = &laby[4][1];
    Stack<Cell*> path = Stack<Cell*>();

    cout << "has path =" <<  labyrinth( laby, ss, tt, path) << endl;
    while (!path.empty()) {
        Cell* c = path.pop();
        cout << "(" << c->x << "," << c->y << ") <-- ";
    }
    cout << endl;


//has path =1
//(4,1) <-- (4,2) <-- (4,3) <-- (4,4) <-- (5,4) <-- (5,3) <-- (5,2) <-- (6,2) <-- (6,3) <-- (6,4) <-- (7,4) <-- (7,5) <-- (8,5) <-- (8,4) <-- (9,4) <-- (9,5) <-- (10,5) <-- (10,4) <-- (10,3) <-- (10,2) <-- (10,1) <-- (11,1) <-- (11,2) <-- (11,3) <-- (11,4) <-- (11,5) <-- (11,6) <-- (11,7) <-- (10,7) <-- (10,8) <-- (9,8) <-- (8,8) <-- (8,9) <-- (8,10) <-- (9,10) <-- (9,11) <-- (8,11) <-- (7,11) <-- (6,11) <-- (5,11) <-- (4,11) <-- (4,10) <-- (4,9) <-- 

队列

queue.png

队列可用于循环分配器模型中,即轮值(round robin) 算法中:

RoundRobin { //循环分配器
    Queue Q(clients); //参不资源分配癿所有客户组成队列Q
    while (!ServiceClosed()) { //在服务兲闭乀前,反复地
        e = Q.dequeue(); //队首癿客户出队,幵
        serve(e); //接叐服务,然后
        Q.enqueue(e); //重新入队
    }
}

模拟银行业务处理过程

//列队使用
//银行服务模拟
//
struct Customer{ //顾客类:
    int window; //所属窗口(队列)
    unsigned int time; //需要的服务时长
    int id;
};

int bestWindow( Queue<Customer> windows[], int nWin) { //为新到顾客确定最佳队列
    int minSize = windows[0].size(), optiWin = 0; //最优队列(窗口)
    for ( int i=1; i< nWin; i++)
        if (minSize > windows[i].size() ) { //挑选出队列最短者
            minSize = windows[i].size();
            optiWin = i;
        } 

    return optiWin;
}

//模拟在银行中接受服务的过程
void simulate( int nWin, int servTime) { //按指定窗口数,服务总时间模拟银行业务
    Queue<Customer>* windows = new Queue<Customer>[nWin]; //为每一窗口创建一个队列

    for (int now=0; now<servTime; now++) { //在下班前,每隔一个单位时间
        if (rand() % (1+nWin)) { //新顾客以 nWin/(nWin+1) 的概率到达
            Customer c;
            c.id = now;
            c.time = 1 + rand() % 98; //新顾客到达,服务时长随机确定
            c.window = bestWindow(windows, nWin); //找出最佳(最短)的服务窗口
            windows[c.window].enqueue(c); //新顾客入对应的队列
            cout << "Customer " << c.id << " enters Queue " << c.window << endl;
        }

        for (int i=0; i< nWin; i++) //分别检查
            if (!windows[i].empty()) //各非空队列
                if (--windows[i].front().time <= 0) { // 队首顾客的服务时长减少一个单位
                    Customer c = windows[i].dequeue(); //服务完毕的顾客出列,由后继顾客接替
                    cout << "Customer " << c.id << " leaves Queue " << c.window << endl;
                }
    } //for

    for (int i=0; i<nWin; i++){
        cout << "Queue " << i+1 << " Size:" << windows[i].size() << endl;
    }
    
    delete [] windows;
}

simulate(10, 60*8);

参考

]]>
数据结构 C++ 版本笔记--3.列表 2018-03-16T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/dsacpp-list 三、列表

列表中元素的前驱后续索引关系,用位置 position 表示,元素 “循位置访问” call-by-position,或 call-by-link,如同通过你的朋友找到他的朋友。

header_trailer.png

封装时,对象中始终包含两个哨兵节点(sentinel node) 头节点 header 和尾节点 trailer。而真正的第一个节点和最后一个节点称为首节点 first node 和 末结点 last node。

引用哨兵节点能简化算法的描述与实现,避免对各种分界退化情况做专门处理。

插入排序 insertion sort

适用于包括向量与列表在内的任何序列结构。

思路:始终将整个序列视作并切分为两部分,有序的前缀 s[0, r) 和无序了后缀 S[r, n);通过迭代,反复地将后缀的首元素转移到前缀中。

由此亦看出插入排序算法的不变性。

在任何时刻,相对于当前节点 e=S[r], 前缀 S[0, r) 总是业已有序。

insertionsort.png

借助有序序列的查找算法,可在该前缀中定位到不大于 e 的最大元素,再将 e 从无序后缀中取出,并紧邻于查找返回的位置之后插入。

//插入排序
template <typename T> //列表的插入排序:对起始于位置 p 的 n 个元素排序
void List<T>::insertionSort( ListNodePosi(T) p, int n) { //valid(p) && rank(p) + n <= size
    for (int r=0; r<n; r++) { //逐一为各节点
        // search(e, r, p) 返回 p 的 r 个真前驱中不大于 e 的最后者位置
        insertA( search(p->data, r, p), p->data); 
        p = p->succ; //转向下一节点
        remove(p->pred);
    }
} //O(n^2)

//有序列表的查找
//返回的位置应便于后续的(插入等)操作
template <typename T> //在有序列表内节点 p(可能是 trailer) 的 n 个真前驱中,找到 <= e 的最后者
ListNodePosi(T) List<T>::search( T const& e, int n , ListNodePosi(T) p ) const {
    // assert: 0 <= n <= rank(p) < _size
    while( 0 <= n--) //对于 p 的最近的 n 个前驱,从右到左逐个比较
        if ( ((p=p->pred)->data) <= e) //直到命中,数值越界或范围越界
            break;
    //assert: 至此位置 p 必符合输出语义约定--尽管此前最后一次关键码比较可能没有意义(等效于与 -inf比较)
    return p; //返回查找终止的位置
} //失败时,返回区间左边界的前驱(可能是 header) --调用者可通过 valid() 判断成功与否
//O(n)

向后分析 backward analysis

backwardAnalysis.png

插入排序时,分析第 r 个元素插入完成的时刻。在独立均匀分布的情况下,L[r] 插入到前面的 r+1 个位置的概率都是相同的,都是 1/(r+1),而插入各位置所需的操作次数分别是 0, 1, … r,从而 S[r] 表示花费时间的数学期望是 [r+(r-1)+…+1+0]/(r+1) + 1 = r/2 + 1

从而知道第 r 个元素的插入期望值为 r/2+1,从而总体元素的期望,即全部元素的期望的总和即为插入排序的平均时间复杂度,为 $O(n^2)$。

选择排序 selection sort

也适用于向量与列表之类的序列结构。

构思: 将序列划分为无序的前缀 S[0, r) 及有序的后缀 S[r, n),此后还要求前缀中的元素都不大于后缀中的元素。如此,每次只需从前缀中选出最大者,并作为最小元素转移至后缀中,即可使有序部分的范围不断扩张。

selectionSort.png

//选择排序
//将序列划分为无序的前缀 S[0, r) 及有序的后缀 S[r, n),此后还要求前缀中的元素都不大于后缀中的元素。如此,每次只需从前缀中选出最大者,并作为最小元素转移至后缀中,即可使有序部分的范围不断扩张。
template <typename T> //列表的选择排序算法,对起始于位置 p 的 n 个元素排序
void List<T>::selectionSort ( ListNodePosi(T) p, int n) { //valid(p) && rank(p)+n <= size
    ListNodePosi(T) head = p->pred;
    ListNodePosi(T) tail = p;
    for (int i=0; i<n; i++)
        tail = tail->succ; //将 head 和 tail 指向排序区列表的 header 和 tailer

    while( 1<n ) { //在至少还剩下两个节点之前,在待排序区间内
        ListNodePosi(T) max = selectMax( head->succ, n); //找出最大者
        insertB( tail, remove(max) ); // 将无序前缀中的最大者移到有序后缀中作为首元素
        // swap(tail->pred->data, max->data); // 优化:可以不用按上面进行删除和插入操作,只需互换数值即可, 习题 3-13
        tail = tail->pred;
        n--;
    }
} //O(n^2)


template <typename T> //从起始于位置 p 的 n 个元素中选出最大者,相同的返回最后者
ListNodePosi(T) List<T>::selectMax( ListNodePosi(T) p, int n) {
    ListNodePosi(T) max = p; //最大者暂定为首节点 p
    for ( ListNodePosi(T) cur = p; 1 < n; n--) //从首节点 p 出发,将后续节点逐一与 max 比较
        if ((cur=cur->succ)->data >= max->data) //若当前元素 >= max, 则
            max = cur;
    return max; //返回最大节点位置
}

归并排序 merge sort

template <typename T> //有序列表的归并:当前列表中自 p 起的 n 个元素,与列表 L 中自 q 起的 m 个元素归并
void List<T>::merge( ListNodePosi(T) &p, int n, List<T>& L, ListNodePosi(T) q, int m) {
    //assert: this.valid(p) && rank(p)+n<=_size && this.sorted(p,n)
    //        L.valid(q) && rank(q)+m<=L._size && L.sorted(q,m)
    //注:在归并排序之类的场合,有可能 this==L && rank(p)+n=rank(q)
    //为方便归并排序,归并所得的有序列表依然起始于节点 p
    ListNodePosi(T) pp = p->pred; //方便之后能返回 p

    while ( 0 < m ) //在 q 尚未移出区间之前
        if ( (0<n) && (p->data <= q->data) ){ //若 p 仍在区间内且 v(p) <= v(q)
            if ( q == ( p=p->succ ) ) // 如果此时 p 部分已经处理完,则提前返回
                break;
            n--;  // p 归入合并的列表,并替换为其直接后继
        }
        else { //若 p 已超出右界或 v(q) < v(p) 则
            ListNodePosi(T) bb = insertB( p, L.remove( (q=q->succ)->pred )); //将 q 转移到 p 之前
            m--;
        }

    p = pp->succ; //确定归并后区间的起点
}


template <typename T> //列表的归并排序算法:对起始于位置 p 的 n 个元素排序
void List<T>::mergeSort( ListNodePosi(T) & p, int n) { //valid(p) && rank(p)+n <= _size
    if (n<2) 
        return;

    int m = n >> 1; //以中点为界
    ListNodePosi(T) q = p;
    for ( int i=0; i<m; i++) //均分列表
        q = q->succ; 

    mergeSort(p, m);
    mergeSort(q, n-m); //对前后子列表排序

    merge(p, m, *this, q, n-m); //归并
}//注意:排序后,p 依然指向归并后区间的起点

ListNodePosi(int) create_node(int data) {
    ListNodePosi(int) node = new ListNode<int>();
    node->data = data;
    return node;
}

倒置

//习题 3-18,共 3 种实现方式
template <typename T>
void List<T>::reverse() {  //适合 T 类型不复杂,能在常数时间内完成赋值操作的情况
    ListNodePosi(T) p = header;
    ListNodePosi(T) q = trailer;
    for (int i=0; i<_size; i+=2){ //从首末节点开始,由外而内,捉对地
        /*p = p->succ;              // 交换对称节点的数据项
        q = q->pred;
        swap(p->data, q->data);
        */
        swap( (p=p->succ)->data, (q=q->pred)->data );
    }
}


template <typename T>
void List<T>::reverse2() {  //适合 T 类型复杂,不能在常数时间内完成赋值操作的情况
    if (_size < 2)
        return;

    ListNodePosi(T) p; ListNodePosi(T) q;

    for ( p = header, q = p->succ; p != trailer; p = q, q = p->succ )
        p->pred = q; //自前向后,依次颠倒各节点的前驱指针

    for ( p = header, q = p->pred; p != trailer; p = q, q = p->pred )
        q->succ = p; //自前向后,依次颠倒各节点的后续指针

    // 准备互换头尾
    trailer->pred = NULL;
    header->succ = NULL;
    swap( header, trailer);
}

template <typename T>
void List<T>::reverse3() {  //适合 T 类型复杂,不能在常数时间内完成赋值操作的情况
    if (_size < 2)
        return;

    for ( ListNodePosi(T) p = header; p; p = p->pred ) //自前向后,依次
        swap(p->pred, p->succ);
    swap(header, trailer);
}

参考

]]>
数据结构 C++ 版本笔记--2.向量 2018-03-14T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/dsacpp-vector 二、向量

线性结构统称为序列 sequence,又根据数据项的逻辑次序与其物理存储地址的对应关系,进一步分为向量 vector 和列表 list。

  1. 向量:物理存放位置与逻辑次序吻合,此时的逻辑次序也称为秩 rank。
  2. 列表:逻辑上相邻的数据项在物理上未必相邻。

从数组到向量

C 等语言中的数组能组织相同类型的数据项,并用下标存取数据项。元素的物理地址和下标满足线性关系,故称为线性数组 linear array。

向量是线性数组的一种抽象和泛化,它也是由具有线性次序的一组元素构成的集体, $ V = { V_0, V1, \cdots, V_{n-1} }$,其中的元素分别由秩 (rank) 相互区别(相当于下标)。各元素的秩互异,且均为 [0, n) 内的整数,若元素 e 有 r 个前驱元素,则其秩为 r。通过 r 亦可唯一确定 $e = V_r$,称为 循秩访问 call-by-rank

向量实际规模 size 与其内部数组容量 capacity 的比值 size/capacity 称装填因子 load factor,用来衡量空间利用率。

可扩充向量

extendable_vector.png

满员时进行插入操作,会自动扩容为原数组为两倍。

template <typename T> void Vector<T>::expand() { //向量空间不足时扩容
    if (_size < _capacity)
        return; //尚未满员时,不必扩容

    if (_capacity < DEFAULT_CAPACITY)
        _capacity = DEFAULT_CAPACITY; //不低于最小容量

    T* oldElem = _elem;
    _elem = new T[_capacity <<= 1]; //容量加倍
    for (int i=0; i<_size; i++)
        _elem[i] = oldElem[i]; //复制原向量内容,T 为基本类型,或已重载赋值操作符 '='

    delete [] oldElem; //释放原空间
}

分摊分析

自动扩容时的插入操作,时间代价为 O(2n)=O(n),但之后至少要再经过 n 次插入后,才会再次扩容操作,故分摊复杂度不高。

足够多次连续操作后,将期间消耗的时间分摊至所有的操作,分摊平均至单次操作的时间成本,称为分摊运行时间 (amortized running time),它与平均运行时间(average running time) 有本质不同,后者是按照某种假定的概率分布,对各情况下所需执行时间的加权平均,故亦称为期望运行时间(expected running time);前者则要求,参与分摊的操作必须构成和来自一个真实可行的操作序列,且该序列还必须足够长。

分摊时间为 O(1)

考虑最坏情况,即都是插入操作,定义:

  • size(n) = 连接插入 n 个元素后向量的规模
  • capacity(n) = 连接插入 n 个元素后向量的容量
  • T(n) = 为连续插入 n 个元素而花费于扩容的时间
  • N 为初始容量

则 size(n) = N + n,既然不溢出,则装填因子不超过 100%,同时,只在满员时将容量加倍,则因子不低于 50%,则:

$size(n) \le capacity(n) \lt 2 \bullet size(n)$

因此,capacity(n) 和 size(n) 同阶: $capacity(n) = \Theta(size(n)) = \Theta(n)$

容量以 2 为比例按指数增长,在容量达到 capacity(n) 前,共做过 $\Theta(log_2n)$ 次扩容,每次扩容时间线性正比于当时的容量(或规模),故扩容累计时间:

$T(n) = 2N + 4N + 8N + \cdots + capacity(n) < 2 \bullet capacity(n) = \Theta(n)$

单次分摊运行时间为 O(1)

其它扩容策略

早期采用追加固定数目的单元,在最坏情况下,分摊时间的下界为 $\Omega(n)$

缩容

template <typename T> void Vector<T>::shrink(){ //装填因子过小时压缩向量所占空间
    if (_capacity < DEFAULT_CAPACITY<<1) //不致收缩到 DEFAULT_CAPACITY 以下
        return;

    if (_size<<2 > _capacity) // 以 25% 为界,大于 25% 时不收缩
        return;

    T* oldElem = _elem;
    _elem = new T[_capacity >>= 1]; //容量减半
    for (int i=0; i<_size; i++)
        _elem[i] = oldElem[i];

    delete [] oldElem;
}

这里缩容阈值是 25%,为避免出现频繁交替扩容和缩容,可选用更低的阈值,甚至取 0(禁止缩容)。分摊时间也是 O(1)。

向量整体置乱算法 permute()

template <typename T> void permute(Vector<T>& V) { //随机置乱向量,使各元素等概率出现于每一位置
    for (int i=V.size(); i>0; i--) //自后向前
        swap(V[i-1], V[rand() % i]); //V[i-1] 与 V[0, i) 中某一随机元素交换,rand() 返回 0~MAX之间的整数
}

permute.png

无序查找

vector_unsorted_find.png

template <typename T> //无序向量的顺序查找,返回最后一个元素 e 的位置;失败时返回 lo-1
Rank Vector<T>::find(T const& e, Rank lo, Rank hi) const {  //在 [lo, hi) 内查找
    //assert: 0 <= lo < hi <= _size
    while ((lo < hi--) && (e != _elem[hi]))
        ; // 自后向前,顺序查找
    return hi; //若 hi<lo, 则意味着失败; 否则 hi 即命中元素的秩
}

复杂度最坏情况是 O(hi-lo)=O(n),最好情况是 O(1),故为输入敏感的算法。

在 r 位置插入

vector_insert.png

template <typename T> //将 e 作为秩为 r 的元素插入
Rank Vector<T>::insert(Rank r, T const& e) {
    //assert: 0 <= r <= size
    expand(); //若有必要, 扩容
    for (int i=_size; i>r; i--)
        _elem[i] = _elem[i-1]; //自后向前, 后继元素顺序后移一个单元

    _elem[r] = e; 
    _size++;
    return r;
}

复杂度为 O(n)。

删除 V[lo, hi)

vector_remove.png

template <typename T> int Vector<T>::remove(Rank lo, Rank hi) { //删除区间 [lo, hi)
    if (lo == hi) 
        return 0; //出于效率考虑,单独处理退化情况,比如 remove(0, 0)

    while (hi < _size)
        _elem[lo++] = _elem[hi++]; // [hi, _size] 顺次前移 hi-lo 个单元

    _size = lo; // 更新规模,直接丢弃尾部 [lo, _size=hi) 区间
    shrink(); //若有必要,则缩容
    return hi-lo; //返回被删除元素的数目
}

复杂度主要消耗于后续元素的前移,线性正比于后缀的长度。

无序向量去重(唯一化)

vector_unsorted_deduplicate.png

template <typename T> int Vector<T>::deduplicate(){ //删除无序向量中重复元素(高效版本)
    int oldSize = _size;
    Rank i = 1; //从 _elem[1] 开始
    while (i < _size) //自前向后逐一考查各元素 _elem[i]
        (find(_elem[i], 0, i) < 0) ? //在其前缀中寻找与之雷同者(至多一个)
            i++ : remove(i); //若无雷同则继续考查其后续,否则删除雷同者

    return oldSize - _size; //向量规模变化量,即被删除元素总数
}

每次迭代时间为 O(n),总体复杂度 $O(n^2)$。

有序向量去重 (唯一化)

低效版本

//有序向量重复元素删除算法(低效版本)
template <typename T> int Vector<T>::uniquify(){
    int oldSize = _size;
    int i = 1;
    while (i<_size) //自前向后,逐一比对各对相邻元素
        _elem[i-1] == _elem[i] ? remove[i] : i++; //若雷同,则删除后者; 否则转到后一元素
    return oldSize-_size; //返回删除元素总数
}

vector_nonefficious_uniquify.png

极端情况下(即元素都相同时),remove() 操作的时间问题: $(n-2)+(n-3)+ \cdots + 2+1=O(n^2)$

改进

以上版本复杂度过高根源在:相邻的相同元素都是一个一个删除的,不是一次性连续删除。

由于有序,每组重复元素都必然前后紧邻集中分布,故可整体删除。

高效版本:

//有序向量重复元素删除算法(高效版本)
template <typename T> int Vector<T>::uniquify(){
    Rank i = 0, j = 0; //各对互异 "相邻“ 元素的秩
    while (++j < _size) //逐一扫描,直到末元素
        if (_elem[i] != _elem[j]) //跳过雷同者
            _elem[++i] = _elem[j]; //发现不同元素时,向前移至紧邻于前者右侧

    _size = ++i; //直接截除尾部多余元素
    shrink();
    return j - i; //返回删除元素总数
}

vector_sorted_uniquify.png

算法复杂度是 O(n)。

有序向量的查找

减而治之,二分查找(版本A)

vector_binSearch_A.png

//二分查找版本A:在有序向量的区间 [lo, hi) 内查找元素 e, 0 <= lo <= hi <= _size
template <typename T> static Rank binSearch(T* A, T const& e, Rank lo, Rank hi) {
    while (lo < hi) { //每步迭代可能要做两次比较判断,有三个分支
        Rank mi = (lo + hi) >> 1; //以中点为轴点
        if (e < A[mi]) 
            hi = mi; //深入前半段 [lo, mi)继续查找
        else if ( e > A[mi])
            lo = mi + 1; //深入后半段
        else
            return mi; //在 mi 处命中
    } //成功查找可以提前终止
    return -1; //查找失败
} //有多个命中元素时,不能保证返回秩最大者; 查找失败时,简单返回 -1, 而不能指示失败的位置

最多 $log_2(hi-lo)$ 次迭代,时间复杂度 O(logn)

查找长度 search length

指查找算法中元素大小比较操作的次数。

binSearch_searchLength.png

二分查找时,查找过程(模式)一致,因此每个元素的查找长度只与该元素的秩和总长度有关,与元素的具体值无关。

长度为 $n=2^k-1$ 的有序向量,平均成功查找长度为 $O(1.5k)=O(1.5log_2n)$

查找失败时必有 lo>=hi,其时间复杂度为 $\Theta(logn)$,平均失败查找长度也为 $O(1.5k)=O(1.5log_2n)$

二分查找时,进入左侧树只要 1 次比较,而进入右侧树要 2 次比较,因此不平衡,从而会出现 1.5 系数。

Fibonacci 查找,按黄金分割比确定 mi

fibSearch.png

该算法会拉长 1 次比较的子向量(左侧树),缩短 2 次比较的子向量(右侧树),从而减少平均查找长度。

// Fibonacci 查找版本A:在有序向量的区间 [lo, hi) 内查找元素 e, 0 <= lo <= hi <= _size
template <typename T> static Rank fibSearch(T* A, T const& e, Rank lo, Rank hi) {
    Fib fib(hi-lo); //用 O(log_phi(hi-lo) 时间创建 Fib 数列,值不大于 hi-lo
    while (lo < hi) { //每步迭代可能要做两次比较,有三个分支
        while (hi-lo < fib.get())
            fib.prev(); //通过向前顺序查找(分摊O(1)) 
        Rank mi = lo + fib.get() - 1; //确定形如 Fib(k)-1 的轴点
        if (e < A[mi])
            hi = mi;
        else if (A[mi] < e)
            lo = mi + 1;
        else
            return mi; //命中
    } //成功查找可以提前终止
    return -1; //查找失败
} //有多个命中元素时,不能保证返回秩最大者; 查找失败时,简单返回 -1, 而不能指示失败的位置

平均查找长度为 $1.44log_2n$,常系数上有改善。

二分查找(版本 B)

从三分支改为两分支,从而使两分的子向量的比较次数都为 1,在切分点 mi 处只任一次 < 比较,判断成功进入前端 A[lo, mi) 继续,否则进入后端 A[mi, hi) 继续。

binarySearch_v2.png

//二分查找版本B:在有序向量的区间 [lo, hi) 内查找元素 e, 0 <= lo <= hi <= _size
template <typename T> static Rank binSearch(T* A, T const& e, Rank lo, Rank hi) {
    //循环在区间不足两个时中止
    while (1 < hi-lo) { //每步迭代仅做一次比较判断,有两个分支;成功查找不能提前终止
        Rank mi = (lo + hi) >> 1; //以中点为轴点
        (e < A[mi]) ? hi = mi : lo = mi; //经比较后确定深入 [lo, mi) 或 [mi, hi)
    } // 出口时 lo+1 == hi, 即区间中只剩下一个元素 A[lo]
    return (e == A[lo]) ? lo : -1; //查找成功时返回对应的秩,否则统一返回 -1 
} //有多个命中元素时,不能保证返回秩最大者; 查找失败时,简单返回 -1, 而不能指示失败的位置

命中时不能及时返回,最好情况下效率有倒退,作为补偿,最坏情况下效率有提高,因此各分支的查找长度更接进,整体性能更趋稳定(好)。

二分查找 (版本 C),返回的结果方便进行插入操作

  • 当有多个命中元素时,必须返回最靠后(秩最大)者
  • 失败时,应返回小于 e 的最大都(仿哨兵 A[lo-1])
//二分查找版本C:在有序向量的区间 [lo, hi) 内查找元素 e, 0 <= lo <= hi <= _size
template <typename T> static Rank binSearch_VC(T* A, T const& e, Rank lo, Rank hi) {
    while (lo < hi) { //每步迭代仅做一次比较判断,有两个分支
        Rank mi = (lo + hi) >> 1; //以中点为轴点
        (e < A[mi]) ? hi = mi : lo = mi + 1; //经比较后确定深入 [lo, mi) 或 (mi, hi)
    } //成功查找不能提前终止
    return --lo; //循环结束时,lo 为大于 e 的元素的最小秩,故 lo-1 即不大于 e  的元素的最大秩
} //有多个命中元素时,总能保证返回秩最大者;查找失败时,能够返回失败的位置

binSearch_v3.png

算法正确性通过数据归纳为:其循环体具有如下不变性:

A[0, lo) 中的元素皆 <= e; A[hi, n) 中的元素皆 > e

  1. 当 lo=0 且 hi=n 时,A[0, lo) 和 A[hi, n) 均空,不变性自然成立。
  2. 在上图 (a) 中,设某次进入循环时以上不变性成立,以下有两种情况:
  3. e<A[mi] 时,如 (b) 中,令 hi=mi,则右侧(包含 mi元素)的 A[hi, n) 中的元素都 >=A[mi]>e
  4. 反之,当 e>=A[mi] 时,如 (c)中,令 lo=mi+1,则左则(包含 mi 元素)的 A[0, lo) 中的元素都 <=A[mi]<=e。从而不变性必然延续。
  5. 循环终止时,lo=hi, 考查此时元素 A[lo-1], A[lo]: 作为 A[0, lo) 的最后一个元素, A[lo-1] <= e,作为 A[lo, n) = A[hi, n) 内的第一个元素,e < A[lo],从而返回 lo-1 即为向量中不大于 e 的最大秩。

假设有序向量中各元素均匀且独立分布,查找时分隔轴点 mi 根据查找值动态计算,从而提高收敛速度。

Interpolation Search

平均情况:每经一次比较,查找范围从 n 缩短为 根号 n。O(loglogn)。

与其它查找算法比较,优势不太明显。

综合使用:

  • 在大规模情况下:用插值查找快速缩小规模
  • 中规模:折半查找
  • 小规模:顺序查找

复杂度下界 lower bound

即最坏情况下的最低成本(worst-case optimal)。

比较树

将基于比较的分支画出比较树。

comparision_tree.png

  • 每一内部节点对应一次比对操作。
  • 内部节点的分支,对应比对下的执行方向。
  • 叶节点对应于算法某次执行的完整过程及输出。
  • 算法的每一次运行过程都对应于从根到某一叶节点的路径。

基于比较的算法(散列等除外)都是 comparison-based algorithm,即 CBA 算法。

从而将 CBA 算法的下界问题转为对应比较树的界定问题。

算法的每一运行时间,取决于对应叶节点到根节点的距离(称作叶节点的深度),而最坏情况下的运行时间,取决于所有叶节点的最大深度(即树的高度)。

一个 CBA 算法对应一棵比较树,从而下界即为所有比较树的最小高度)。

在一个高度为 h 的二叉树中,叶结点不可能多于 $2^h$,反过来,若某一问题的输出结果(即叶结点)不于少 N 种,则树高不可能低于 $log_2N$。

从而下界即与输出的结果数目 N 相关。

排序

排序的下界

CBA 式排序算法中,当有 n 个元素时,可能输出有 N = n! 种,比较树是三叉树(对应小于、相等、大于),从而高度为 $log_3 (n!)$ 的上确,从而 排序算法的下界为 $\Omega(log_3(n!)) = \Omega(nlogn)$。桶排序和基数排序不是 CBA 算法,不基于比较树,从而不是该下界。

起泡排序

template <typename T> //向量的起泡排序
void Vector<T>::bubbleSort(Rank lo, Rank hi) //assert: 0 <= lo < hi <= size
{
    while(!bubble(lo, hi--)) //逐趟扫描交换,直到全序
        ; // pass
}

template <typename T> bool Vector<T>::bubble(Rank lo, Rank hi) { //一趟扫描交换
    bool sorted = true; //整体有序标志
    while (++lo < hi) //自左向右,逐一检查各对相邻元素
        if (_elem[lo-1] > _elem[lo]) { //若逆序,则
            sorted = false; //意味着尚末整体有序,并需要
            swap(_elem[lo-1], _elem[lo]);
        }
    return sorted;
}

算法中只有相邻元素的前一个大于后者时,能会交换,保证了重复元素间的相对次序在排序前后的一致,即算法具有稳定性。

//优化的起泡排序
//每趟扫描后,记录最右侧的逆序对位置,
//从而下趟可直接忽略后面已经就序的元素
template <typename T>
void Vector<T>::bubbleSort2(Rank lo, Rank hi)
{
    while (lo < (hi = bubble2(lo, hi)))
        ; //pass
}

template <typename T> Rank Vector<T>::bubble2(Rank lo, Rank hi) {
    Rank last = lo; //最右侧的逆序对初始化为 [lo-1, lo]
    while (++lo < hi) //自左向右,逐一检查各对相邻元素
        if (_elem[lo-1] > _elem[lo]) { //若逆序,则
            last = lo; //更新最右侧逆序对位置
            swap(_elem[lo-1], _elem[lo]);
        }
    return last;
}

归并排序

有序向量的二路归并 (2-way merge),将两有有序序列合并成为一个有序序列:

迭代进行,每次迭代时只比较两个序列的首元素,将小者取出放在输出序列末尾。最后将另一个非空的向量整体接到输出向量的末尾。

2WayMerge.png

template <typename T> //向量归并排序
void Vector<T>::mergeSort(Rank lo, Rank hi) { // 0 <= lo < hi <= size
    if (hi-lo < 2) //单元素区间自然是有序
        return;

    int mi = (lo+hi) >> 1; //中点为界
    mergeSort(lo, mi); mergeSort(mi, hi); //分别对前后半段排序
    merge(lo, mi, hi); //归并
}

template <typename T> //有序向量的归并
void Vector<T>::merge(Rank lo, Rank mi, Rank hi){ //以 mi 为界,合并有序子向量 [lo, mi), [mi, hi)
    T* A = _elem + lo; //前子向量的首地址,合并后的结果地址也从这开始

    int first_len = mi-lo;
    T* B = new T[first_len]; //用于临时存放前子向量
    for (Rank i=0; i<first_len; B[i] = A[i++])
        ; //pass

    int second_len = hi-mi;
    T* C = _elem + mi; //后子向量的首地址

    for (Rank i=0, j=0, k=0; (j<first_len) || (j<second_len); ){ //将 B[j] 和 C[k] 中的小者续至 A 末尾

        // 前子向量还有元素未处理时,
        //   1. 如果后子向量已经处理完毕,或者
        //   2. 其第一个元素小于后子向量的第一个元素
        if ( (j<first_len) && ( !(k<second_len) || (B[j]<=C[k]) ) )
            A[i++] = B[j++];

        // 后子向量还有元素未处理时,
        //   1. 如果前子向量已经处理完毕,或者
        //   2. 其第一个元素小于前子向量的第一个元素
        if ( (k<second_len) && ( !(j<first_len) || (C[k] < B[j]) ) )
            A[i++] = C[k++];
    }

    delete [] B;
} //归并后得到完整的有序向量 [lo, hi)

二路归并时间与归并元素个数成线性关系,为 O(n)。

整体排序时,分而治之共分了 $log_2n$ 层,每层的 n 个元素进行归并,因此复杂度为 O(nlogn)。

mergeSortInstance.png

二路归并的精简实现:

由于后子向量本身就位于结果向量后,如果前子向量提前处理完后,对后子向量的复制操作无需进行:

mergeSort_v2.png

位图

习题 [2-34] 位图(Bitmap)是一种特殊癿序列结极,可用以劢态地表示由一组(无符号)整数极成癿集合。 其长度无限,且其中每个元素癿叏值均为布尔型(刜始均为 false)。

//习题 2-34 位图 Bitmap b)
class Bitmap {
    private:
        char* M; int N; //比特图所存放的空间 M[], 容量为 N * sizeof(char) * 8 比特。

    protected:
        void init(int n) {
            M = new char[N = (n+7)/8]; //申请能容纳 n 个比特的最少字节
            memset(M, 0, N);
        }

    public:
        Bitmap(int n=8) {
            init(n);
        }

        Bitmap(char* file, int n=8) { //从指定文件中读取比特图
            init(n);
            FILE* fp = fopen(file, "r");
            fread(M, sizeof(char), N, fp);
            fclose(fp);
        }

        ~Bitmap() {
            delete [] M;
            M = NULL;
        }

        void set(int k){ //将第 k 位置设置为 true
            expand(k);

            /*
             * k>>3 确定该位在哪个字节
             * k&0x07 确定字节中的位置
             * (0x80 >> (k & 0x07)) 将字节中的该位置 1
             */
            M[k >> 3] |= (0x80 >> (k & 0x07) );
        }

        void clear(int k){ //将第 k 位置设置为 false
            expand(k);

            /*
             * k>>3 确定该位在哪个字节
             * k&0x07 确定字节中的位置
             * (0x80 >> (k & 0x07)) 将字节中的该位置 1
             * ~(0x80 >> (k & 0x07)) 将字节中的该位置 0
             */
            M[k >> 3] &= ~(0x80 >> (k & 0x07));
        }

        bool test(int k){ //测试第 k 位是否为 true
            expand(k);

            /*
             * k>>3 确定该位在哪个字节
             * k&0x07 确定字节中的位置
             * (0x80 >> (k & 0x07)) 将字节中的该位置 1
             */
            return M[k >> 3] & (0x80 >> (k & 0x07) );
        }

        void dump(char* file) { //将位图整体导出至指定的文件,以便以后的新位图批量初始化
            FILE* fp = fopen(file, "w");
            fwrite(M, sizeof(char), N, fp);
            fclose(fp);
        }

        char* bits2string(int n) { //将前 n 位转换为字符串
            expand(n-1); //此时可能被访问的最高位为 bit[n-1]
            char* s = new char[n+1];
            s[n] = '\0'; //字符串所占空间,由上层调用者负责释放
            for (int i=0; i<n; i++)
                s[i] = test(i) ? '1' : '0';
            return s;
        }

        void expand(int k) { //若被访问的 Bitmap[k] 已出界,则需扩容
            if (k < 8*N)
                return;
            int oldN = N;
            char* oldM = M;
            init(2*k); //与向量类似,加倍策略
            memcpy(M, oldM, oldN);
            delete [] oldM;
        }
};

    //习题 [2-34] Bitmap b) 测试
    cout << "Bitmap test:" << endl;
    Bitmap bitmap = Bitmap();
    bitmap.set(0);
    bitmap.set(1);
    bitmap.set(9);
    cout << "Bitmap:" << bitmap.bits2string(15) << endl; //110000000100000

以上实现中,要花费时间初始化,通过空间换时间,下面的实现能节省初始化所有元素所需的时间。

//习题 2-34 c)
//创建 Bitmap 对象时,如何节省下为初始化所有元素所需的时间?
//设位置只需提供 test() 和 set() 接口,暂时不需要 clear() 接口,
class Bitmap_without_init { //以空间换时间,仅允许插入,不支持删除
    private:
        Rank* F; Rank N; //规模为 N 的向量 F,
        Rank* T; Rank top; //容量为 N 和栈

    protected:
        inline bool valid(Rank r){ return (0 <= r) && (r < top); }

    public:
        Bitmap_without_init(Rank n=8) {
            N = n;
            F = new Rank[N]; T = new Rank[N]; // 在 O(1) 内隐式地初始化
            top = 0; 
        }

        ~Bitmap_without_init(){ delete [] F; delete [] T; }

        //接口
        inline void set(Rank k) {
            if (test(k))
                return;
            //要设置的位置 k,对应的 F[k] 处将值设置为栈的栈顶指针,
            //同时在栈中将栈顶指针处将值设置为 k,建立校验环
            //从而当要 test k 位置时,取出对应的 F[k] 处的值,即为当时
            //保存的栈顶指针,再从栈中取出值,如果值和 k 相同,则
            // k 位有设置值。
            T[top] = k; F[k] = top; ++top; //建立校验环
        }

        inline bool test(Rank k) {
            return valid(F[k]) && ( k == T[ F[k] ] );
        }

        char* bits2string() { //将前 n 位转换为字符串
            char* s = new char[N+1];
            s[N] = '\0'; //字符串所占空间,由上层调用者负责释放
            for (int i=0; i<N; i++)
                s[i] = test(i) ? '1' : '0';

            return s;
        }

};

    //习题 [2-34] 无需初始化时间的 Bitmap c) 测试
    cout << "Bitmap_without_init test:" << endl;
    Bitmap_without_init bitmap2 = Bitmap_without_init(10);
    bitmap2.set(0);
    bitmap2.set(1);
    bitmap2.set(9);
    cout << "Bitmap:" << bitmap2.bits2string() << endl; //1100000001

初始化时开辟两个长度为 N 的连续空间 F,T,F 存储要设置值的秩,T 作为堆栈。

当要设置的位置为 k,对应的 F[k] 处将值设置为栈的栈顶指针,同时在栈中将栈顶指针处将值设置为 k,建立校验环。 从而当要 test k 位置时,取出对应的 F[k] 处的值,即为当时保存的栈顶指针,再从栈中取出值,如果值和 k 相同,则 k 位有设置值。

下面是依次标记 B[4], B[11], B[8], B[1], B[14] 的一个运行实例:

bitmap_without_init_instance.png

如果要支持 clear(k) 操作,则必须能辨别两种无标记的位:从末标记过的和曾经标记后又被清除的。

下面的实现中将清除后的 k 位,其对应的栈中的值约定为 -1-k。

//习题 2-34 c)
//创建 Bitmap 对象时,如何节省下为初始化所有元素所需的时间?
//如果还要支持 clear() 接口,则必须有效辨别两种无标记的位:从末标记过的
//和曾经标记后又被清除的。
//下面的实现中将清除后的 k 位,其对应的栈中的值约定为 -1-k。
class Bitmap_without_init2 { //以空间换时间,仅允许插入,支持删除
    private:
        Rank* F; Rank N; //规模为 N 的向量 F,
        Rank* T; Rank top; //容量为 N 和栈

    protected:
        inline bool valid(Rank r){ return (0 <= r) && (r < top); }
        inline bool erased(Rank k) {// 判断 [k] 是否曾经被标记过,后又被清除
            return valid (F[k]) &&
                (T[ F[k] ] == -1-k); //清除后的栈中值约定为 -1-k
        }

    public:
        Bitmap_without_init2(Rank n=8) {
            N = n;
            F = new Rank[N]; T = new Rank[N]; // 在 O(1) 内隐式地初始化
            top = 0; 
        }

        ~Bitmap_without_init2(){ delete [] F; delete [] T; }

        //接口
        inline void set(Rank k) {
            if (test(k))
                return;
            //要设置的位置 k,对应的 F[k] 处将值设置为栈的栈顶指针,
            //同时在栈中将栈顶指针处将值设置为 k,建立校验环
            //从而当要 test k 位置时,取出对应的 F[k] 处的值,即为当时
            //保存的栈顶指针,再从栈中取出值,如果值和 k 相同,则
            // k 位有设置值。
            //
            if (!erased(k)) //若初始标记,则创建新校验环,
                F[k] = top++; //
            T[ F[k] ] = k;  //若系曾经标记后被清除的,则恢复原校验环
        }

        inline void clear(Rank k) {
            if (test(k))
                T[ F[k] ] = -1-k;
        }

        inline bool test(Rank k) {
            return valid(F[k]) && ( k == T[ F[k] ] );
        }

        char* bits2string() { //将前 n 位转换为字符串
            char* s = new char[N+1];
            s[N] = '\0'; //字符串所占空间,由上层调用者负责释放
            for (int i=0; i<N; i++)
                s[i] = test(i) ? '1' : '0';

            return s;
        }
};

    //习题 [2-34] 无需初始化时间的 Bitmap c) 支持 clear()测试
    cout << "Bitmap_without_init2 test:" << endl;
    Bitmap_without_init2 bitmap3 = Bitmap_without_init2(10);
    bitmap3.set(0);
    bitmap3.set(1);
    bitmap3.set(9);
    cout << "Bitmap:" << bitmap3.bits2string() << endl; //1100000001
    bitmap3.clear(1);
    cout << "Bitmap:" << bitmap3.bits2string() << endl; //1000000001

Eratosthenes 筛法求不大于 n 的所有素数

素数(质数)为 > 1 的除 1 和自身外不能被其它整数整队的整数。

Eratosthenes 筛法是将 1-n 中逐渐排除合数,剩下的就都是素数,具体为:

先标识 0 和 1,排除它们,因为它们不是素数。从 2 开始一直到 n,设当前值为 i,由于之前没有被排除,则它是素数,此时标识排除以 i 为倍数的数(2i, 3i, … ki)。

下面是前 3 次迭代排除合数的过程:

eratosthenes_instance.png

从确定一个素数 i 开始(比如 i=5),实际上完全可以直接从 $i^2$ (而不是 2i) 开始,删除剩下的相关合数,因为介于 [2i, $i^2$] 之间的均已经在之前的某次迭代中被筛除了。

同理,若只考查不超过 n 的素数,则当 $i> \sqrt n$ 后,外循环即可终止。

eratosthenes_tuned.png

//习题 2-36 利用 Bitmap 计算出不大于 10^8 的所有素数 Eratosthenes 筛法
// ,因此 0, 1 都不是质数。
// 筛法求素数:计算不大于 n 的所有素数
//   先排除 0, 1 两个非素数,从 2 到 n 迭代进行:
//     接下来的数 i 是一个素数,并将素数的整数倍 (i, 2i, ... ki) 都标识为非素数。
// 根据素数理论,不大于 N 的素数最多 N/ln(N) 个。
void Eratosthenes(int n, char* file) {
    Bitmap bm(n);
    bm.set(0); bm.set(1); //0 和 1 都不是素数
    for (int i=2; i<n; i++) //反复地从
        if (!bm.test(i))   //下一个可认定的素数 i 起
            for (int j= min(i, 46340)*min(i, 46340); j<n; j += i) //以 i 为间隔
                bm.set(j); //将下一个数标记为合数
    B.dump(file); //将所有整数的筛法标记统一存入指定文件。
}

参考

]]>
数据结构 C++ 版本笔记--1.绪论 2018-03-07T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/dsacpp-intro 一、绪论

算法要素:

  • 输入与输出
  • 基本操作、确定性与可行性、有穷性
  • 有穷性 finiteness, 正确性 correctness
  • 退化(degeneracy, 即极端情况)与鲁棒性(robustness)
  • 重用性

证明算法有穷性和正确性的技巧:从适当的角度审视整个计算过程,并找出其所具有的某种不变性和单调性。单调性指算法会推进问题规模递减,不变性则不仅在算法初始状态下自然满足,而且应与最终的正确性相响应,当问题规模减小时,不变性应随即等价于正确性。

有穷性

Hailstone(n) 序列:

例子:

Hailstone(42) = {42, 21, 64, 32, …, 1} Hailstone(7) = {7, 22, 11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, …, 1} Hailstone(27) = {27, 82, 41, 124, 62, 31, 94, 47, 142, 71, 214, 107, … }

它会持续下降,但不会持续上升,其变化捉摸不定,与冰雹运动过程非常想像:有时上升,有时下降,当为 1 时表示落地。

序列长度与 n 不成正比。

Hailstone(n) 是否有穷,现在还没有定论。

时间复杂度

time complexity 是输入规模 n 的一个函数,有所有的 n! 种输入中,选择执行时间最长者记为 T(n),作为算法的时间复杂度。

利用理想计算模型,如图灵机模型或 RAM 模型,将算法的运行时间计算转化成算法的基本操作次数的计算。

渐进复杂度 (主流长远)

即只关注算法在输入规模大时的复杂度,用大 O 记号(big-o notation) 来表示 T(n) 的渐进上界。

定时,若存在正常数 c 和函数 f(n),使得对任何 n » 2 都有 $T(n) \le c \bullet f(n)$,则可认为在 n 足够大后,f(n) 给出了 T(n) 增长速度的一个渐进上界,记为: T(n) = O(f(n))。

大 O 记号的性质:

  • 对于任一常数 c > 0, 有 $O(f(n)) = O(c \bullet f(n))$,即函数各项的正的常系数可忽略并等同于 1
  • 对于任意常数 a > b > 0, 有 $O(n^a + n^b) = O(n^a)$,即多项式中的低次项可忽略。

在 O 记号确定上界,大 $\Omega$ 记号确定下界,而 大 $\Theta$ 记号给出了一个确界,是一个准确估计:

time_complexity.png

复杂度分析

1. 常数时间复杂度算法 constant-time algorithm, O(1)

2. 对数复杂度 O(logn)

整数 n 二进制展开的位数为 $1+\lfloor log_2 n \rfloor$

由于当 c >0 时, $log_ab = log_cb / log_ca$, 因此对于任意 a, b > 0, $log_an = log_ab \bullet log_bn$,根据 O 记号定义,有 $log_r n$ 界定时,常底数 r 具体取值无所谓,故对数复杂度直接记为 O(logn),为 logarithmic-time algorithm。

而对数多项式复杂度表示为 $T(n) = O(log^c n), c>0$,为对数多项式时间复杂度算法,polylogarithmic-time algorithm。其效率无限接近于常复杂度。

3. 线性 O(n), linear-time algorithm

4. 多项式 O(polynomial(n)), polynomial-time algorithm

实际应用中,一般认为这种复杂度是可接受的,其问题是可有效求解或易解的 (tractable)。

对于任意 c>1,$n^c = O(2^n)$,即指数是多项式的上界,相应地,前者是后者的下界。

2-Subset 子集问题: 集合 S 中包含 n 个下整数,$\sum S = 2m$,则 S 是否有子集 T,满足 $\sum T = m$

实例:美国大选,51 个州共 538 票,各州票数不同,获得 270 票即当选,问是否会出现恰好各得 269 票?

直觉算法:逐一枚举每一子集统计,复杂度为 $2^n$

有 n 个项的集合,子集共有 $2^n$ 个(考虑每个项包含和不包含在子集中的情况),遍历 0 ~ $2^n$,每个值转成二进制,依次判断:

// 习题 [1-16]
//2-Subset 子集问题
int subset(int A[], int n, int m) {
    int end = (1<<n)-1; //2^n - 1
    for (int mask=0; mask<=end; mask ++) {
        int sum = 0;
        for(int i=0; i<n; i++) {
            if (mask & (1<<i)) {
                sum += A[i];
            }
        }
        if (sum == m)
            return mask; //mask 中的各个位数 1 代表所含的项
    }
    return 0;
}


int main() {
    int A[6] = {1, 2, 3, 4, 2, 6}; //sum=18=2X9
    cout << "subset(A, 6, 9)=" << subset(A, 6, 9) << endl;  //14, 1110, 表示 {2, 3, 4} 子集符合
}  

定理: 2-Subset is NP-complete,目前没有多项式复杂度的算法。

5. 指数 $O(2^n)$ exponential-time algorithm

指数复杂度算法无法真正应用于实际中。

复杂度层次

complexity_scale.png

复杂度分析的方法

  • 迭代: 级数求和
  • 递归: 递归跟踪 + 递推方程
  • 猜测 + 验证

级数

算数级数: 与末项平方同阶

$T(n) = 1+2+ \cdots +n = n(n+1)/2 = O(2^n)$

幂方级数,:比幂次高出一阶:

$T_2(n) = 1^2 + 2^2 + 3^2 + \cdots + n^2 = n(n+1)(2n+1)/6 = O(n^3)$

$T_3(n) = 1^3 + 2^3 + 3^3 + \cdots + n^3 = n^2(n+1)^2/4 = O(n^4)$

$T_4(n) = 1^4 + 2^4 + 3^4 + \cdots + n^4 = n(n+1)(2n+1)(3n^2+3n-1)/30 = O(n^5)$

几何级数(a>1):与末项同阶

$T_a(n) = a^0 + a^1 + \cdots + a^n = (a^{n+1}-1)/(a-1) = O(a^n)$

$T_2(n) = 1 + 2 + 4 + \cdots + 2^n = (2^{n+1}-1) = O(2^{n+1}) = O(2^n)$

等差级数:之和与其中最大一项的平方同阶

$x + x+d + x+2d + \cdots + x+(n-1)d = (d/2)n^2 + (x-d/2)n = \Theta(n^2)$

等比级数之和与其中最大一项同阶

$x + xd + xd^2 + \cdots + xd^{n-1} = nx + x(d^n-1)/d-1 = \Theta(d^n)$

收敛级数: O(1)

可能未必收敛,但长度有限:

调和级数: $h(n) = 1 + 1/2 + 1/3 + \cdots + 1/n = \Theta(logn)$

对数级数: $log1 + log2 + log3 + \cdots + logn = log(n!) = \Theta(nlogn)$

书: Concrete Mathematics

循环 vs. 级数

循环次数的统计转为级数的计算。

次数对应面积:

intro_loop_counter_vs_area.png

次数对应级数和,几何级数与末项同阶:

intro_loop_counter2.png

intro_loop_counter3.png

封底估算, Back-of-The-Envelope Calculation

即在信封或不用纸笔在脑中进行估算。

$1 天 = 24 hr X 60min X 60sec \approx 25 X 4000 = 10^5 sec$

$1 生 \approx 100 yr = 100 X 365 day = 3 X 10^4 day = 3 X 10^9 sec$

即 300 年为 $10^{10} sec$,三生三世为 $10^{10}sec$

宇宙大爆炸至今为 $10^{21} = 10 X (10^{10})^2 sec$

fib(n) = $O(\Phi ^ n)$, 其中 $\Phi$ 是黄金分割点的值 1.168…,

而 $\Phi ^ {36} = 2^{25}$, (36 为 6 的平方, 25 为 5 的平方)。

$\Phi ^ 5$ 约为 10

$\Phi ^ 3$ 约为 $2^2$

递归

线性递归

// 数组求和算法,线性递归版
int sum_linear_recursion(int A[], int n) {
    if (n < 1) // 平凡情况,递归基
        return 0;
    else //一般情况
        return sum_linear_recursion(A, n-1) + A[n-1]; //递归:前 n-1 项之和,再累计第 n-1 项
} //O(1)*递归深度 = O(1)*(n+1) = O(n)

平凡情况称为 递归基 base case of recursion,用于结束递归。

sum_linear_recursion 算法是线性递归:每一递归实例对自身的调用至多一次,每一层上最多一个实例,且它们构成一线性次序关系。

这种形式中,问题总可分解成两个独立子问题:其一对应于单独的某元素,如 A[n-1],故可直接求解;另一个对应剩余部分,且其结构与原问题相同,如 A[0, n-1],最后,子问题解经过简单合并,可得到原问题的解。

线性递归模式,往往对应于 减而治之 (decrease-and-conquer) 的算法策略:递归每深入一层,问题的规模都缩减一个常数,直到最终蜕化为平凡的小(简单)问题。

递归分析

递归跟踪法(recursion trace)

  1. 算法的第一递归实例都表示为一个方框,其中注明该实例调用的参数
  2. 若实例 M 调用实例 N,则在 M 与 N 对应的方框之间添加一条有向联线

recursion_trace.png

递推方程法 (recurrence equation)

通过对递归模式的数学归纳,导出复杂度定界函数的递推方程(组)及边界条件,从而将复杂度的分析,转化为递归方程(组)的求解。

而对递归基的分析通常可获得边界条件。

比如上面的数组求和算法,设长度为 n 时的时间成本为 T(n),为解析 sum_linear_recursion(A, n),需递归解决 sum_linear_recursion(A, n-1),再加上 A[n-1],则: $T(n) = T(n-1) + O(1) = T(n-1) + c_1$,$c_1$ 为常数

而抵达递归基时, sum_linear_recursion(A, 0) 只需常数时间,则 $T(0) = O(1) = c_2$,联立以上两个方程,得到:

$T(n) = c_1 n + c_2 = O(n)$

多向递归

$2^n$ 的求解。

一般的定义为:

这是一个线性递归,复杂度为 O(n)

若 n 的二进制展开式为 $b_1b_2b_3\cdots b_k$,则:

$2^n = (\cdots(((1\times2^{b_1})^2 \times 2^{b_2})^2 \times 2^{b_3})^2 \cdots \times 2^{b_k})$

则 $n_{k-1}$ 和 $n_k$ 的幂值关系有:

$2^{n_k} = (2^{n_{k-1}})^2 \times 2^{b_k}$, 由归纳得递推式:

typedef long long tint64;

inline tint64 square(tint64 a) { return a*a; }

tint64 power2(int n) { //幂函数 2^n算法,优化递归版本, n>=0
    if (0==n) return 1; //递归基
    return (n&1) ? square(power2(n >> 1)) << 1 : square(power2(n >> 1)); //视 n 的 奇偶分别递归
} // O(logn) = O(r), r 为输入指数 n 的比特位数

迭代版本为:

typedef long long tint64;
//迭代版本
tint64 power2_loop(int n) {
    tint64 pow = 1; // 累积器初始化为 2^0
    tint64 p = 2; // 累乘项初始化为 2,对应最低位为 1 的情况
    while (n>0) { // 迭代 log(n) 轮
        if (n&1) // 根据当前比特位是否为 1, 决定
            pow *= p; // 将当前累乘项计入累积器
        n >>= 1; //指数减半
        p *= p; //累乘项自乘
    }
    return pow; 
} // O(logn) = O(r), r 为输入指数 n 的比特位数

//而一般性的 a^n 计算如下 
tint64 power_loop(tint64 a, int n) { // a^n 算法: n >= 0
    tint64 pow = 1; // 累积器初始化为 2^0
    tint64 p = a; // 累乘项初始化为 a,对应最低位为 1 的情况
    while (n>0) { // 迭代 log(n) 轮
        if (n&1) // 根据当前比特位是否为 1, 决定
            pow *= p; // 将当前累乘项计入累积器
        n >>= 1; //指数减半
        p *= p; //累乘项自乘
    }
    return pow; 
} // O(logn) = O(r), r 为输入指数 n 的比特位数

递归消除

尾递归及消除

递归调用为算法的最后一步操作(即递归的任一实例都终止于这一递归调用),为尾递归,它们可转换为等效的迭代版本。

但上面数组求和的线性递归版本中,最后的操作是加法运算,不是纯递归调用,因此不是尾递归,但是也可以转换成迭代版本。

// 数组倒置,将尾递归优化为迭代版本
void reverse_loop_version(int* A, int lo, int hi){
    while (lo < hi) {
        //swap(A[lo++], A[hi--]); //交换 A[lo], A[hi], 收缩待倒置区间
        int tmp = A[lo];
        A[lo] = A[hi];
        A[hi] = tmp;
        lo++;
        hi--;
    }
} // O(hi-lo+1)

二分递归

使用 分而治之 divide-and-conquer 的策略,将问题持续分解成更小规模的子问题,至平凡情况。

和减而治之策略一样,也要对原问题重新表述,保证子问题与原问题在接口形式上一致。

每一递归实例都可能做多次递归,故称作 多路递归 multi-way recursion。通过是将原问题二分,故称 二分递归 binary recursion

数组求和采用二分递归:

// 数组求和算法,二分递归版本,入口为 sum_binary_recursion(A, 0, n-1)
int sum_binary_recursion(int A[], int lo, int hi) {
    if (lo == hi) //如遇递归基(区间长度已降至 1),则
        return A[lo]; //直接返回该元素
    else { // 否则是一般情况 lo < hi, 则
        int mi = (lo+hi) >> 1; //以居中单元为界,将原区间一分为二
        return sum_binary_recursion(A, lo, mi) + sum_binary_recursion(A, mi+1, hi); //递归对各子数组求和,然后合计
    }
} //O(hi-lo+1),线性正比于区间的长度

当 $n=2^m$ 的形式下的 n=8 时,递归跟踪分析为:

sum_binary_recursion.png

其递归调用关系构成一个层次结构(二叉树),每降一层,都分裂为一个更小规模的实例。经过 $m=log_2 n$ 将递归调用,数组区间长度从 n 首次缩减为 1, 并到达第一个递归基。

其递归调用深度不超 m+1,每个实例仅需常数空间,故空间复杂度是 O(m+1) = O(logn),比线性递归版本的空间复杂度 O(n) 优。

递归调用次数是 2n-1,故时间复杂度也是 O(2n-1)=O(n)。

分治递归要有效率,要保证子问题之间相互独立,不做重复递归。

Fibonacci 数的二分递归:

// 计算 Fibonacci 数列的第 n 项,二分递归版本,O(2^n)
tint64 fibonacci_binary_recursion(int n) {
    return (n<2) ? 
        (tint64) n // 若到达递归基,直接取值
        : fibonacci_binary_recursion(n-1) + fibonacci_binary_recursion(n-2);
}

fib(n) = $O(\Phi ^ n)$, 其中 $\Phi$ 是黄金分割点的值 1.168…,

而 $\Phi ^ {36} = 2^{25}$, (36 为 6 的平方, 25 为 5 的平方)。

其时间复杂度是 $2^n$,原因是计算中出现的递归实例的重复度极高。

优化策略,消除递归算法中重复的递归实例

即:借助一定量的辅助空间,在各子问题求解后,及时记录下其对应的解。

  1. 从原问题出发自顶向下,遇到子问题时,先查验是否计算过,避免重新计算,为制表(tabulation) 或记忆 (memoization) 策略。
  2. 从递归基出发,自底而上递推地得出各子问题的解,直到最终原问题的解,即为动态规划(dynamic programming) 策略。

Fibonacci 数:线性递归

fib(n-1) 和 fib(n-2) 并非独立,将递归函数改为计算一对相邻的 Fib 数:

// Fibonacci 线性递归版本,入口形式 fibonacci_linear_recursion(n, prev)
// 使用临时变量,避免重复递归计算
tint64 fibonacci_linear_recursion(int n, tint64& prev) {
    if (n == 0){ //若到达递归基,则
        prev = 1;
        return 0; // 直接取值: fib(-1) = 1, fib(0)=0
    } else {
        tint64 prevPrev; prev = fibonacci_linear_recursion(n-1, prevPrev); //递归计算前两项
        return prevPrev + prev; //其和即为正解
    }
} // 用辅助变量记录前一项,返回数列的当前项, O(n)

通过 prevPrev 调阅此前的记录,从而省略了 fib(n-2) 的递归计算,呈线性递归模式,递归深度线性正比于输入值 n,共出现 O(n) 的递归实例,时间和空间复杂度都为 O(n)。

Fibonacci 数:动态规划

// Fibonacci 迭代版本: O(n)
// 采用动态规划策略,按规模自小而大求解各子问题
tint64 fibonacci_loop_version(int n) {
    tint64 f = 0, g = 1; // 初始化 fib(0)=0, fib(1)=1
    while (0 < n--) {
        g += f; f = g-f; //依原始定义,通过 n 次加法和减法计算 fib(n)
    }
    return f;
}

最长公共子序列 LCS 问题:

intro_LCS.png

递归版本 LCS(A[0,n], B[0,m]):

两个序列 A[0, n], B[0, m]

  1. 若 n=-1 或 m=-1,则返回空串,这是一个递归基。
  2. 自后向前比较,或 A[n] == B[m] == x,则取作 LCS(A[0, n), B[0, m)) + x,减而治之。
  3. 若 A[n] != B[m],这里分 2 种情况,一种是 B[m] 对 LCS 无贡献,此时最终值为 LCS(A[0, n], B[0, m)); 另一种是 A[n] 对 LCS 无贡献,此时最终值为 LCS(A[0, n), B[0, m]),返回这 2 种情况的最长者。

复杂度为 $2^n$。

这和 Fib() 类似,有大量重复的递归实例(子问题)。

各子问题,分别对应于 A 和 B 的某前缀组合,总共有 O(nm) 种。 采用动态规划策略,只需 O(nm) 时间。

  1. 将所有子问题(假想地)列表一个表
  2. 颠倒计算方向,从 LCS(A[0], B[0]) 出发,依次计算所有项,并填写表格。
  3. 对于每个单元,如果减而治之情况(元素匹配),则值取左上值+1,如果是分而治之(不匹配),则值为左边和右边值的最大值。

intro_LCS_dynamic_programming.png

动态规划能消除重复。

参考

]]>
Android Fragment 中使用 TTF 矢量字体图标(以 Font Awesome 4.7.0 为例) 2018-03-02T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/android-fragment-with-awesome-font 创建 Assets 文件夹

在 Android Studio 中将项目显示方式改为 Project,在 app/src/main 上右键 New -> Folder -> Assets Folder

app/src/main/assets 上右键 New -> Directory -> 填写 font

将下载的 fontawesome-webfont-ttf 文件复制到新建的 assets/font/ 下。

编写资源文件,将图标代码与资源名称绑定

<!--/res/values/strings.xml 文件-->
<string name="fa_car">&#xf1b9;</string>
<string name="fa_apple">&#xf179;</string>
<string name="fa_android">&#xf17b;</string>

图标代码与名称对应关系见 Font Awesome Cheatsheet

布局文件中的 TextView 使用 Font Awesome

<!-- in layout file fragment.xml -->
<TextView
    android:id="@+id/tv"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="8dp"
    app:layout_constraintStart_toEndOf="@+id/textView8"
    app:layout_constraintTop_toTopOf="@+id/textView8"
    android:textSize="24sp"
    android:textColor="@color/colorAccent"
    android:text="@string/fa_android" />

Fragment 中创建字体,并设置 TextView 使用该字体

public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment, container, false);
    Typeface font = Typeface.createFromAsset(getActivity().getAssets(), "font/fontawesome-webfont.ttf");
    TextView tv = (TextView)view.findViewById(R.id.tv);
    tv.setTypeface(font);
    //...
}

如果你想改变图标大小和颜色,只要修改字体的大小和颜色即可,也就是说只要修改 TextView 的 textSize 和 textColor。

参考

]]>
在 Ubuntu 16.04 上配置 SSR 客户端并通过 Privoxy 转换为 HTTP 代理 2018-01-31T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/setup-ssr-http-proxy-on-ubuntu Ubuntu SSR 客户端

github 上下载最新的 SSR 客户端脚本,下载为 ~/opt/ssr.sh

设置权限,安装和配置:

$ cd ~/opt
$ chmod 766 ./ssr.sh
$ ./ssr.sh install
$ ./ssr.sh config

配置信息格式为:

{
  "server": "x.x.x.x",
  "server_ipv6": "::",
  "server_port": 2333,
  "local_address": "127.0.0.1",
  "local_port": 1080,
  "password": "xxx",
  "group": "xxx",
  "obfs": "tls1.2_ticket_auth",
  "method": "aes-192-ctr",
  "ssr_protocol": "auth_sha1_v4",
  "obfsparam": "",
  "protoparam": "",
  "udpport": "0",
  "uot": "0"
}

可以在 https://ssssssssjshhd.herokuapp.com/ 等网站上获取免费 SSR 信息。

安装配置 Privoxy

$ sudo apt-get install privoxy

配置文件位置: /etc/privoxy/config

  • # listen-address localhost:8118 这行注释去掉。
  • 在配置文件最后添加两行:
    • forward-socks5 / 127.0.0.1:1080 .
    • listen-address 127.0.0.1:8118

运行 sudo service privoxy start 即可。

日志文件位置: /var/log/privoxy/logfile

Android Studio 3.0.1 模拟器中使用代理

模块器中设置代理的方式:

  • 在模拟器设置页中设置为使用 Android Stuido 的代理,或者手动设置为 http://127.0.0.1:8118 的代理。
  • 在 Android Terminal 中使用 cd ~/Android/Sdk/tools && emulator -avd Nexus_5X_API_23 -http-proxy http://127.0.0.1:8118 -debug-proxy 开启模块器并设置代理。

再 2 种方法都没有尝试成功,虽然在 Privoxy 的日志文件中都有连接记录,但是模拟器的浏览器中访问被墙网站都 TIMED_OUT,但是访问未被墙网站正常。

最后尝试直接在 Java 网络连接代码中使用代理,尝试成功:

import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.Proxy;

//in class ...
public byte[] getUrlBytes(String urlSpec) throws IOException {
    URL url = new URL(urlSpec);
    Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("10.0.2.2", 8118));
    HttpURLConnection connection = (HttpURLConnection) url.openConnection(proxy);

    try {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        InputStream in = connection.getInputStream();

        if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
            throw new IOException(connection.getResponseMessage() +
                ": with " + urlSpec );
        }

        int bytesRead = 0;
        byte[] buffer = new byte[1024];
        while ((bytesRead = in.read(buffer)) > 0) {
            out.write(buffer, 0, bytesRead);
        }
        out.close();
        return out.toByteArray();
    } finally {
        connection.disconnect();
    }
}

其中 10.0.2.2 地址为 Android 模拟器中访问开发机的地址。

参考

]]>
Android 笔记 2018-01-29T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/android-notes Android 基本知识
  1. Activity 子类用来管理用户界面。
  2. layout 是一个 XML 文件,用来定义一组用户界面对象及它们在屏幕上的位置。
  3. 多个 widget 组成一个用户界面,每个 widget 都是 View 及其子类的一个实例,如 TextView, Button。
  4. widget 属性 layout_width, layout_height, 值为 match_parent 表示撑满父 widget(另一个过时的值为 fill_parent),而 wrap_content 表示满足其内容所需的大小。
  5. AppCompatActivity 是 Activity 的子类,实现了对旧版本的兼容功能。
  6. Activity 实例化时会调用自身的 onCreate(Bundle savedInstanceState) 方法用来初始化,该方法中要调用 setContentView(layoutResID) 来设置要管理的用户界面。
  7. 资源及资源 ID src/main/res/ 目录下,例如 res/layout/activity_quiz.xml, res/values/string.xml 等。Android 会根据这些资源定义自动生成一个 R.java 文件,位于 build/generated/source/r/debug 下。这个文件自动与所有源代码文件关联,因此可以在源代码中使用 R.string.app_name 的形式引用资源 ID。
  8. 布局 XML 文件中指定的单个 widget 不会自动关联一个资源 ID,如果要在源代码中引用这些 widget,需要手动指定,如:
<Button
android:id="@+id/true_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/true_button" />

这里 android:id="@+id/true_button" 表示创建的资源 ID 为 true_button,可以使用 Activity 的方法 findViewById(R.id.true_button) 获取该 widget。

  1. Activity 是 Context 的子类,可以用在 Toast.makeText(Context context, int resId, int duration) 等中。

build 过程

res 下的所有资源文件(XML)由 aapt 工具(Android Asset Packaging Tool) 编译成一个编译过的资源文件和 R.java 文件,src/ 下的源代码编译成 Java 的字节码,再 cross compile 成 Dalvik 字节码(.dex),最后资源文件和 .dex 文件合并签名成一个 apk 文件。

build 工具

使用 Gradle 工具来管理构建过程。

打开 Android Studio 下边的 Terminal, 运行 $ ./gradlew tasks 将下载并显示所有可用的 task。

运行 $ ./gradlew installDebug 将应用安装到连接的设备上。

MVC

Model 层保存应用的数据和业务逻辑。

View 层一般是由布局文件创建的界面。

Controller 层关联 View 和 Model,是应用逻辑所有。一个 Controller 一般就是 Activity, Fragment, Service 子类。

Screen pixel density:

  • mdpi medium-density screens (~160dpi)
  • hdpi high-density screens (~240dpi)
  • xhdpi extra-high-density screens (~320dpi)
  • xxhdpi extra-extra-high-density screens (~480dpi)

Activity 的生命周期

Activity State Diagram

当设备的配置有修改时,例如切换了设备的方向,Android 会销毁并重装创建 Activity,以应用最适合当前配置的资源配置信息,例如将 res/layout/ 下的默认布局切换为 res/layout_land/ 下的横屏布局。设备配置的修改情况还有:键盘的显示和关闭,语言的修改等。

Layout XML 中的 tools

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    tools:context="com.bignerdranch.android.geoquiz.CheatActivity">

   
    <TextView
        android:id="@+id/answer_text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="24dp"
        tools:text="Answer"/>

</LinearLayout>

添加 xmlns:tools 命名空间后,可以用 tools:attr 覆盖子 widget 上的属性值,用以将该值显示在预览界面中。例如 TextViewtext 属性,则 tools:text 可以设置该 widget 在预览界面上的值,相当于是 spaceholder。

Starting an Activity

Starting an Activity

所有组件(Activities, services, broadcast receivers, content providers) 都通过 intent 进行通信。Intent 类提供多种构造器,适用于多种用途。

用于开启 Activity 的 Intent 构造器: public Intent(Context packageContext, Class<?> cls)

Class 参数指定要开启的 Activity 类,Context 参数指定要哪个 package 中查找该类。

Activity 间的数据传输

Intent extras

Intent 上使用 public Intent putExtra(String name, boolean value) 等多种形式(值参数有多种)来携带数据进行传送,对方通过 getIntent().getBooleanExtra(name, defaultValue) 取值。

开启 Activity 并返回结果

使用 public void startActivityForResult(Intent intent, int requestCode) 开启。其中 requestCode 是原样返回,当启动多个子 Activity 后,可用来区别是哪个 Activity 返回。

子 Activity 中使用 public final void setResult(int resultCode[, Intent data]) 设置返回结果。其中 resultCode 的值一般为 Activity.RESULT_OKActivity.RESULT_CANCELED。 如果子 Activity 没有调用 setResult 进行设置,当用户点回退键返回父 Activity 时,结果的 resultCode 默认为 RESULT_CANCELED

父 Activity 中要重载 protected void onActivityResult(int requestCode, int resultCode, Intent data) 处理返回的结果。

ActivityManager

系统只有一个 ActivityManger 和一个 Activity 的 Back Stack。

Fragment

UI Fragment 也基于布局文件创建 UI,Activity 可由多个 Fragment 组合起来。

一般使用 support library 中的 Fragment 实现,而不使用本地 SDK 中的实现,这样可以在更低版本中使用。

需要在 app 模块中的 build.gradle 文件中指明依赖: File->Project Structure...->Modules app->Dependencies->+->Library dependency,添加 support-v4

Fragment 生命周期

fragment_lifecycle.png

和 Activity 不同,Fragment 的生命周期方法不是由 OS 调用,而是由其所在的 Activity 管理和调用。

Activity 通过 FragmentManager 管理其所有的 Fragment,以及一个 Back Stack.

fragmentmanager.png

Activity 中的一个容器放置一个 Fragment,因此 FragmentManger 通过容器的 ResID 来查找和标识 Fragment:

FragmentManager fm = getSupportFragmentManager();
Fragment fragment = fm.findFragmentById(R.id.fragment_container);

if (fragment == null) {
    fragment = new CrimeFragment();
    fm.beginTransaction()
         .add(R.id.fragment_container, fragment)
         .commit();
}

当一个 Fragment 被添加到 FragmentManager 后,会依赖调用其生命周期函数 onAttach(Activity), onCreate(Bundle), onCreateView(...),并随着 Activity 的状态同步执行 onStart(), onResume, …

使用 Fragement 原则:屏幕上同时包含的 Fragment 个数不多于 2 或 3 个。

RecyclerView, Adapter, ViewHolder

每个 ViewHolder 实例对应一个列表的行:

ViewHolder.png

RecyclerView 通过 ViewHolder 管理每个具体列表行:

ViewHolder.png

RecyclerView 通过 Adapter 创建 ViewHolder,并与具体的数据模型绑定:

RecyclerView-Adapter.png

RecyclerView 以支持库的形式提供,故要在 app 模块的 build.gradle 中添加依赖:

compile 'com.android.support:recyclerview-v7:27.0.2', 见 https://developer.android.com/topic/libraries/support-library/packages.html 和 https://stackoverflow.com/questions/25477860/error-inflating-class-android-support-v7-widget-recyclerview

RecyclerView 对于显示组件的布局也是委托给布局管理器完成的,因此要设置一个布局管理器:

public class CrimeListFragment extends Fragment {

    private RecyclerView mCrimeRecyclerView;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_crime_list, container, false);
        mCrimeRecyclerView = (RecyclerView) view.findViewById(R.id.crime_recycler_view);
        mCrimeRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
        
        return view;
    }
}

Fragment Result

类似 Activity,Fragment 中也可以 Fragment.startActivityForResult(Intent, int) 启动一个返回结果的 Activity,同时也可以重载 onActivityResult() 来获取返回的结果。 但是只能是 Activity 才有结果,Fragment 不能有结果,因此,Fragment 没有 setResult 方法,只能设置 Activity 的结果: getActivity().setResult(Activity.RESULT_OK, null)

ViewPager

  • ViewPager 类直接继承了ViewGroup类,所有它是一个容器类,可以在其中添加其他的 View 类。
  • ViewPager 类需要一个 PagerAdapter 适配器类给它提供数据。
  • ViewPager 经常和 Fragment 一起使用,并且提供了专门的 FragmentPagerAdapter 和 FragmentStatePagerAdapter 类供 Fragment 中的 ViewPager 使用。

FragmentStatePagerAdapter 节约内存,它会调用 remove(Fragment) 将用不到的 Fragment 销毁。FragmentPagerAdapter 性能更好,但耗内存,它只调用 detach(Fragment),不从内存中删除。

AlertDialog

使用依赖包 AppCompat 中的 android.support.v7.app.AlertDialog, 确保新旧系统上界面风格一致。

AlertDialog 直接显示后,当设备方位转动后退出显示,因此应将 AlertDialog 封装成一个 android.support.v4.app.DialogFragment 中。

运行错误: java.lang.NoSuchMethodError: No static method getFont...: 确保支持包和编译的 SDK 版本都一致,例如:

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "com.bignerdranch.android.criminalintent"
        minSdkVersion 16
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'com.android.support:appcompat-v7:27.0.0'
    compile 'com.android.support:recyclerview-v7:27.0.0'
}

Fragment 间的数据交互

父 -> 子: 子中的 newInstance(data) 方法将由父传入的 data 封装成 Bundle,通过 Fragment.setArguments(bundle) 设置成 Fragment 的参数。父中调用子的 newInstance(data) 获取一个子的实例,子中通过 getArguments().getSerializable(TAG) 获取父传入的数据。

子 -> 父: 父中为子 Fragment 设置结果处理的目标 Fragment 为自身,childFragment.setTargetFragment(parentFragment, REQUEST_CODE),子中通过 Intent 封装返回值,并调用父的 parentFragment.onActivityResult(REQUEST_CODE, resultCode, intent) 传回给父,父中重载 onActivityResult()

父中通过 startActivityForResult() 开启普通的 Fragment,而通过 show 开启 DialogFragment

Toolbar

通过 AppCompat 支持库,可以在 API 7 之后的系统中使用 Toolbar。要使用 Toolbar 功能,确保所有的 Activity 都继承自 AppCompatActivity(它继承于 FragmentActivity)。

Toolbar VS ACtion Bar

Action Bar 总是要在屏幕上方显示,且只能有一个 Action Bar,其尺寸固定。

Toolbar 没有这些限制,尺寸可调整,可以有多个 Toolbar(每个 View 有可设置自己的 Toolbar),Toolbar 中的 View 可设置。

应用的可访问目录

每个应该都有一个 sandbox 目录,以应用的包名命名,在 /data/data 目录下,例如 /data/data/com.bignerdranch.android.criminalintent

编辑器

  • 安装 ideaVIM 插件: File->Settings...->Plugins
  • 设置方向键的快捷键,方便在代码提示框中上下移动: File-Settings...->Keymap->Editor Actions->,Down 改为 Ctrl J,Up 改为 Ctrl K

Implicit Intent

通过 Implicit Intent 通知 OS 开启特定功能的应用。

Intent 中需指定的参数有:

  • action: 指定需开启的应用的功能,如 Intent.ACTION_VIEW 是打开浏览器,Intent.ACTION_SEND 是发送
  • data: 内容的 URI
  • type: 内容的类型,是 MIME 值
  • categories: 可选,描述你 where, when, how 使用打开的应用,例如 android.intent.category.LAUNCHER 表示该 Activity 应显示在 top-level app launcher 中。

每个应用的 Manifest 的 intent-filter 中声明自己的功能及参数,例如声明为浏览器功能:

<activity
    android:name=".BrowserActivity"
    android:label="@string/app_name" >
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:scheme="http" android:host="www.bignerdranch.com" />
    </intent-filter>
</activity>

当应用中通过 Implicit Intent 开启通讯录程序获取部分联系人信息时,通讯录程序返回包含 URI 的 intent 给父 Activity 时,会一并添加 Intent.FLAG_GRANT_READ_URI_PERMISSION,即父 Activity 无需声明要求的权限就能完成此次的权限操作(只这一次)。

获取联系人信息的 Intent

1. 在 AndroidManifest 中加入读写权限

2. Android 系统管理联系人的 URI

  • 获取联系人的 ID 和 NAME: ContactsContract.Contacts.CONTENT_URI
  • 获取联系人的电话号码: ContactsContract.CommonDataKinds.Phone.CONTENT_URI
  • 获取联系人的邮箱地址: ContactsContract.CommonDataKinds.Email.CONTENT_URI

Contacts 有两个表,分别是 rawContact 和 Data,rawContact 记录了用户的 id 和 name,其中 id 栏名称为:ContactsContract.Contacts._ID, name 名称栏为 ContactContract.Contracts.DISPLAY_NAME,电话信息表的外键 id 为ContactsContract.CommonDataKinds.Phone.CONTACT_ID,电话号码栏为:ContactsContract.CommonDataKinds.Phone.NUMBER(字符串值)。

3. 获取手机号码并打开拨打电话界面的代码

private static final int REQUEST_CONTACT_PHONE = 2;

mCallButton = (Button) v.findViewById(R.id.crime_call);
mCallButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent pickContact = new Intent(Intent.ACTION_PICK,
            ContactsContract.CommonDataKinds.Phone.CONTENT_URI);
        startActivityForResult(pickContact, REQUEST_CONTACT_PHONE);
    }
});

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (resultCode != Activity.RESULT_OK) {
        return;
    }

    if (requestCode == REQUEST_CONTACT_PHONE && data != null) {
        Uri contactUri = data.getData();
        String[] queryFields = new String[] {
            ContactsContract.CommonDataKinds.Phone.NUMBER
        };
        Cursor c = getActivity().getContentResolver()
                    .query(contactUri, queryFields, null, null, null);

        try {
            if (c.getCount() == 0) {
                return;
            }

            c.moveToFirst();
            String numberString = c.getString(0);
            Uri number = Uri.parse("tel:" + numberString);

            // to Dial
            Intent dialIntent = new Intent(Intent.ACTION_DIAL, number);
            startActivity(dialIntent);
        } finally {
            c.close();
        }
    }
}

为 Tablet 设置不同的资源值

创建资源时,将 qualifier 设置为 sw600dp,即 smallest width 为 600dp。

  • wXXXdp 表示宽度大于等于 XXX dp 的设备。
  • hXXXdp 表示高度大于等于 XXX dp 的设备。
  • swXXXdp 表示宽度或高度(测试小的那个量)大于等于 XXX dp 的设备。

Assets

相当于打包进应用中的一个文件系统。

但是 Asset 文件路径不能像普通文件一样打开为 File,必须通过 AssetManager 操作:

AssetManager mAssets = context.getAssets();
String assetPath = sound.getAssetPath();
InputStream soundData = mAssets.open(assetPath);

// AssetFileDescriptors are different from FileDescriptors,
AssetFileDescriptor assetFd = mAssets.openFd(assetPath);
// but you get can a regular FileDescriptor easily if you need to.
FileDescriptor fd = assetFd.getFileDescriptor();

Retained Fragment

Fragment 可以设置为 retained(默认为 False),这样当其父 Activity 销毁(如旋转)时,只进行 detach,不进行销毁,从而能保持 Fragment 中的数据和状态的一致性。

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // when a fragment is retained, the fragement is not
        // destroyed with the activity.
        // It is preserved and passed along intact to the new activity.
        setRetainInstance(true);
    }

retained_fragment_life_cycle.png

Style 和 Theme

style 名中用 BaseStyleName.InheritedStyleName 表示 BaseStyleName.InheritedStyleName 继承 BaseStyleName(只有当所有样式代码在相同包中才可以用)。

也可能过 parent 属性值指定父样式。

属性值的访问

在 XML 中,当引用一个具体值,如颜色值时,用 @,例如: @color/gray。当引用主题中的资源时,用 ?,例如: android:background="?attr/colorAccent",表示引用主题中名为 colorAccent 这个属性值。

而代码中引用主题属性值:

Resources.Theme theme = getActivity().getTheme();
int[] attrsToFetch = { R.attr.colorAccent };
TypedArray a = theme.obtainStyledAttributes(R.style.AppTheme, attrsToFetch);
int accentColor = a.getInt(0, 0);
a.recycle();

Launchable app

使用 PackageManager 查询 launchable main activity,即应用的 AndroidManifest.xml 中的 intent-filter 有:

<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

startActivity(Intent) 时, OS 会自动添加 category CATEGORY_DEFAULT 的匹配,故不适合用来启动 Launchable main activity.

Tasks

一个 Task 关联一组开启的应用,用来保存用户的状态及 Back Stack。点击右下角的 Recent tasks list 按钮,可打开 Overview screen, 进行 Task 的切换。

开启新的 Task 只需添加一个 Flag 即可:

Intent i = new Intent(Intent.ACTION_MAIN)
    .setClassName(activityInfo.applicationInfo.packageName,
        activityInfo.name)
    .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(i);

使用 AsyncTask 完成后台任务

private class FetchItemsTask extends AsyncTask<Integer, Void, List<GalleryItem>> {
    @Override
    protected List<GalleryItem> doInBackground(Integer... params) {
        Integer page = params[0];
        return new FlickrFetchr().fetchItems(page);
    }

    @Override
    protected void onPostExecute(List<GalleryItem> items) {
        mItems.addAll(items);
        setupAdapter();
    }
}

new FetchItemsTask().execute(mFlickrFetchrPage++);

其中任务内容写在 doInBackground 中,会在单独纯种中执行,执行完毕后,结果传入 onPostExecute,并在主线程中执行该方法。

每个 AsyncTask 任务实例只能调用一次 execute

使用 AsyncTaskLoader 完成数据任务

Parse JSON in JAVA

Gson 能直接将 JSON 转成 Java 对象。

Message Queue, Looper, Handler, handlerThread

message queue 相当于线程的邮箱,Looper 对象用来管理 message queue, 不断地取出 message 给线程处理。 具有 message queue 和 looper 对象的线程称为一个 message loop,而主线程就是一个 message loop。

自定义的 message loop 可用来完成后台任务,一般继承 HandlerThread 类来实现,因为它已经提供了一个 Looper 对象。

message_loop.png

Message loop 实例初始化例子:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mThumbnailDownloader = new ThumbnailDownloaderHandlerThread<>();
    mThumbnailDownloader.start(); // ensure looper initialized
    mThumbnailDownloader.getLooper();
}

每个 Message 实例内包含 3 个参数:

  • what: 用户定义的 int 型值,描述做什么。
  • obj: 用户定义的随着该 Message 发送的对象
  • target: 用来处理该 Message 的 Handler

handler_looper.png

一个 message loop 中有一个 HandlerThread 和 Looper,一个 Looper 对应一个 Message Queue,Queue 中的每个 Message 可对应不同的 Handler,而一个 Handler 对应一个 Looper。创建 Handler 后,Handler 将绑定其创建时的线程所在的 Looper。

创建 Message 一般通过 Handler.obtainMessage()Message.sendToTarget() 将 Message 发送给其 Handler,Handler 会先将 Message 添加到其 Looper 的 Message Queue 中。

实现 Handler 的 handlerMessage(Message msg) 对 Message 进行处理:

public class ThumbnailDownloaderHandlerThread<T> extends HandlerThread {
    @Override
    protected void onLooperPrepared() {
        mRequestHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                if (msg.what == MESSAGE_DOWNLOAD) {
                    T target = (T) msg.obj;
                    handleRequest(target);
                }
            }
    };
}

对 Message 的处理代码也可以通过 Handler.post(Runnable) 实现:

mResponseHandler.post(new Runnable() {
    public void run() {
    }
});

AsyncTask vs Thread

AsyncTask 用于无需重复执行的短时间内能完成的任务。

图片下载库:

Picasso,包含了下载,缓存,变换、点位符图片等功能。

  1. 下载 picasso-2.5.2.jar 文件,放置到 app/libs 目录下。
  2. 通过 Project Setting... 添加依赖: implementation files('libs/picasso-2.5.2.jar')
  3. 在 HolderView 中:
public void bindGalleryItem(GalleryItem galleryItem) {
            Picasso.with(getActivity())
                    .load(galleryItem.getUrl())
                    .placeholder(R.drawable.bill_up_close)
                    .into(mItemImageView);
}

SearchView

推荐使用支持库的实现: android.support.v7.widget.SearchView

使用 SharedPreferences 存储持久化数据

内容保存在本系统的 sandbox 中。

public class QueryPreferences {
    private static final String PREF_SEARCH_QUERY = "searchQuery";

    public static String getStoredQuery(Context context) {
        return PreferenceManager.getDefaultSharedPreferences(context)
                .getString(PREF_SEARCH_QUERY, null);
    }

    public static void setStoredQuery(Context context, String query) {
        PreferenceManager.getDefaultSharedPreferences(context)
                .edit()
                .putString(PREF_SEARCH_QUERY, query)
                .apply();
    }
}

Service

IntentService

IntentService 类似 Activity,也是 Context 的子类。类似 Activity,将 IntentService 在 AndroidManifest.xml 中注册,从而能响应对应的 Intent,并调用其中的 onHandleIntent(Intent) 方法在后台运行。

IntentService 接收到的 Intent 叫 command,接收到的多个 command 会放在队列中,依次处理。

IntentService_commands.png

// IntentService 类
public class PollService extends IntentService {
    private static final String TAG = "PollService";
    
    public static Intent newIntent(Context context) {
        return new Intent(context, PollService.class);
    }
    
    public PollService() {
        super(TAG);
    }
    
    @Override
    protected void onHandleIntent(Intent intent) {
        Log.i(TAG, "Received an intent: " + intent);
    }
}
// 使用
Intent i = PollService.newIntent(getActivity());
getActivity().startService(i);

使用 AlertManager 定时重复启动 IntentService

public class PollService extends IntentService {
    private static final String TAG = "PollService";

    private static final int POLL_INTERVAL = 1000 * 60; // 60 seconds
    
    public static void setServiceAlarm(Context context, boolean isOn) {
        Intent i = PollService.newIntent(context);
        PendingIntent pi = PendingIntent.getService(context, 0, i, 0);

        AlarmManager alarmManager = (AlarmManager)
                context.getSystemService(Context.ALARM_SERVICE);

        if (isOn) {
            alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
                    SystemClock.elapsedRealtime(), POLL_INTERVAL, pi);
        } else {
            alarmManager.cancel(pi);
            pi.cancel();
        }
    }
}

其中的 PendingIntent.getService(context, 0, i, 0); 封装了 Context.startService(Intent) 的功能。

之后在 Fragment 的 onCreate 中使用 PollService.setServiceAlarm(getActivity(), true); 设置。

通过 AlertManager 设置后,即便应用退出后,AlertManager 也还会在后台定时启动 IntentService。

Notification

使用 NotificationCompat 实现 Notification。

Notification 封装了显示在状态栏中的标题 (Ticker),图标 (SmallIcon),内容组件的标题和内容,一个 PendingIntent(当点击该 Notification 时会启动)。

            Resources resources = getResources();
            Intent i = PhotoGalleryActivity.newIntent(this);
            PendingIntent pi = PendingIntent.getActivity(this, 0, i, 0);

            Notification notification = new NotificationCompat.Builder(this)
                    .setTicker(resources.getString(R.string.new_pictures_title))
                    .setSmallIcon(android.R.drawable.ic_menu_report_image)
                    .setContentTitle(resources.getString(R.string.new_pictures_title))
                    .setContentText(resources.getString(R.string.new_pictures_text))
                    .setContentIntent(pi)
                    .setAutoCancel(true)
                    .build();

            NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
            notificationManager.notify(0, notification);

IntentService 和 Service

Service 类似 Activity,是一种应用组件,它提供各种生命周期回调函数,但是这些函数都在主线程执行。

推荐用 IntentService 实现大多数的服务型任务,因为可以在后台执行。

Broadcast Intent & Receiver

在 Manifest 中注册的 Receiver 当应用未启动状态下也会接收 Broadcast Intent。

在代码中 dynamic broadcast receiver 只在应用运行时会接收:

registerReceiver(BroadcastReceiver, IntentFilter);
unregisterReceiver(BroadcastReceiver);

应用内的通信,Local Events

使用 event bus,每三方库有 greenrobot 的 EventBus,Square 的 Otto。

获取 signing key

在 Android Studio Terminal 中:

运行 ./gradlew signingReport, SHA1 值即为 signing key。

参考

]]>