蒋海云个人博客,日拱一卒。 2018-06-15T10:25:53+08:00 jiang.haiyun#gmail.com flash cards 应用 Anki 的使用 2018-06-13T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/anki 下载

桌面版本,主要用于 Card 的创建和编辑。

手机端 支持 Android (AnkiDroid) 和 iOS (AnkiMobile),其中 iOS 版本的 AnkiMobile 收费。

配置支持 Markdown

  1. 打开桌面版,打开 Tools -> Add-ons,安装 Power Format Pack 插件。

  2. 安装后重开桌面版,打开 Tools -> Power Format Pack (options),开启 Enable Markdown。 之后在编辑 Card 时可直接输入 Markdown 文本,再点击工具栏中的 Markdown 按钮进行显示转换。

配置支持 Latex

需要在桌面系统上安装 Latex 环境,以 Ubuntu 16.04 为例,需安装:

$ sudo apt-get install texlive-base dvipng

之后可在 Card 中直接输入 Latex 公式。

Latex 表达式形如:

  1. 内联式: [latex]\begin{math}...\end{math}[/latex],等同 [$]...[/$]
  2. 独行式: [latex]\begin{displaymath}...\end{displaymath}[/latex], 等同 [$$]...[/$$]

详细说明见 文档

例子:

Hailstone(n) 序列 的例子:

[latex]
$$
Hailstone(n) = \begin{cases}
  \{1\}  & n \le 1 \\
  \{n\} \cup Hailstone(n/2) & \text{n is even} \\
  \{n\} \cup Hailstone(3n+1) & \text{n is odd}
\end{cases}
$$
[/latex]

以及下面的一些例子代码:

例子1:

[latex]
$$ f(x)=\left\{
\begin{aligned}
x & = & \cos(t) \\
y & = & \sin(t) \\
z & = & \frac xy
\end{aligned}
\right.
$$
[/latex]


例子二:

[latex]
$$ F^{HLLC}=\left\{
\begin{array}{rcl}
F_L       &      & {0      <      S_L}\\
F^*_L     &      & {S_L \leq 0 < S_M}\\
F^*_R     &      & {S_M \leq 0 < S_R}\\
F_R       &      & {S_R \leq 0}
\end{array} \right. $$
[/latex]

例子三:

[latex]
$$f(x)=
\begin{cases}
0& \text{x=0}\\
1& \text{x!=0}
\end{cases}$$
[/latex]

Latex 公式在桌面上可编辑后,会编译生成图片版本同步到服务器,手机端看到的是图片。

相关链接

]]>
小程序框架基础 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) || (k<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(n^2)$

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

$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-1})$

收敛级数: 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; // 累积器初始化为 a^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 模拟器中访问开发机的地址。

参考

]]>