《算法导论》笔记(1)排序 含部分习题

为什么是《算法导论》读书笔记?因为这本书是经典中的经典,读这本书对算法学习帮助非常大,读书笔记值得写。而且从这本书的读书笔记开始写博客,也是一个比较容易上手的办法。一为复习算法,一为练笔。

开篇讲算法的基本概念。从插入和归并排序举例说明伪代码的写法,算法的代价,分治的思想。

2.1节有个细节,其实非常值得重视,即循环不变式。这是算法正确性的核心概念。记得曾看过一本Knuth的书《编程的修炼》,这本书市面少见,深图有。这本书通篇都是围绕循环不变式、算法正确性等概念来展开的(不过这本书非必看,虽然精巧而深刻,但是既没有强基础性也没有强实用性,单作为修炼方法可看)。初始为真,循环过程中为真,停机为真,所以算法是正确的。再往深了展开,就是数理逻辑,可计算性,递归论,模型论。这块等有空了再回去翻书,写些笔记。

插入排序算法:每次操作A[i],A[j]两个元素,i,j两套循环。外循环i=2to last, 将A[i]插入到正确的位置;具体插入方法是内循环j=i-1to 1, A[i], A[j] 比较,大于/小于则交换,小于/大于则跳出内循环。算法有了,循环不变式也有了。如果用函数式伪码表述:

function insert(last){ if last==1 return A; compare_exchange(last, insert(last-1));} function compare_exchange(i, A){if i==1 return A; if A[i]> A[i-1] return A; exchange(A[i],A[i-1]);return compare_exchange(i-1, A)}


可见函数式与自然语言更接近,而命令式语言更接近机器语言。这里其实不是纯粹的函数式编程,因为有A储存数据,有exchange A[i]与A[i-1]的方法,更类似JavaScript而非lisp。我叫它软函数式,如果用完全没有语句的硬函数式,

function compare_exchange(i, A){if i==1 return A; if A[i]> A[i-1] return A; return compare_exchange(i-1,exchange(i, A));}; function exchange(){return A after exchanged//略}


函数式语言的优点是语义明确结构优美,问题在于管理内存麻烦,难手动优化。比如说先exchange两项再递归compare_exchage,可以利用一段内存放置A,省事,但是如果没有exchange过程,而是改成exchange函数返回一个新的A作为compare_exchange的参数,那么进栈出栈上下文切换需要额外代价。从自然语言到函数式语言易,但从函数式语言到自然语言难。猜测hacker喜欢lisp,是因为别人不容易看懂,但自己写起来很方便。顺便吐槽:宏是脚本,文本对文本的替换,我觉得宏不是函数式编程。

插入排序的最坏与平均代价略过。

归并排序算法:自然语言是把所有元素按二叉树分割,到最底层后依次合并并上升。这个算法的优点是,不需要所有元素两两比较,即θ(n^2)。因为分割的每层合并时需要θ(n),而层数为log2(n),所以总代价为θ(n*log2(n))。伪函数式

function m_sort(A){if count(A)==1 return A; merge( m_sort(A/2的前一半), m_sort(A/2的后一半)); }function merge(A1,A2){ if A1[1]>A2[1] return (A1[1]+merge(A1-A1[1],A2)) ifA2[1]>A1[1] return (A2[1]+merge(A1,A2-A2[1]));}



 注:+和-是重载的运算符,表示列前后拼接和列去掉一个元素。吐槽:重载运算符…也是写给自己看的,只在伪代码里面用,正式代码尽量不要用。

看看前2章有什么有趣的练习题。

练习2.3-2提到merge()时用哨兵还是个数统计。哨兵的好处是简洁,比较时隐含了个数截止的条件。如果用个数统计,merge()需要附加条件

function merge(A1,A2){return {if A1A2非空 {if A1[1]>A2[1]return (A1[1]+merge(A1-A1[1], A2)) if A2[1]>A1[1] return (A2[1]+merge(A1,A2-A2[1]));} +A1A2残余部分}}


练习2.3-5,2.3-6提到插入排序时用二分查找代替线性查找。命令式程序可以将内循环改为一个二分查找的函数。函数式伪码

function insert(last){ if last==1 return A; binary_search(1,last-1,last, insert(last-1));}function binary_search(i, j, k, A){if i-j==1 insert_here() and return A; if A[k]>A[(i+j)/2]return binary_search((i+j)/2, j, k, A);  if A[k]<A[(i+j)/2] return binary_search(i, (i+j)/2, k, A);}


但是我们需要注意,二分查找是节省了查找时间,但是插入时需要将元素依次移动,所以总的开销不能节省。最坏情况下仍然是n^2。与归并相比,问题主要是层数上。二分是n层,每层最多n次移动。归并只需要log2(n)层。

练习2.3-7,先排序,然后将大于与小于x/2的数分为2边,内外2层循环,外循环从x/2向左遍历左边,内循环从x/2向右遍历右边,内循环跳出条件是和大于x,外循环跳出条件是到最小元素或者找到一对正确解。

Function find(i, j, k, x, A){if i<0 return failure; if A[i]+A[j]>x return find(i-1,k, k,  x, A); if A[i]+A[j]<x return find(i, j+1, k, x, A); if A[i]+A[j]==x return i and j;}


思考题2-2 ,冒泡法的循环不变式是每次循环的冒泡最高点即最大或最小值。

思考题2-3,插入排序时,每一次比较即纠正一个逆序对。A[1 to k]来看,逆序对的总和是对每个元素的逆序对个数求和,而且调整A[k]前面的元素顺序,不会影响A[k]逆序对数量。所以按照A[2]…A[k]的顺序插入时,插入A[i]就纠正了A[i]的逆序对数量,再插入A[i+1]就纠正了A[i+1]的逆序对数量。根据数学归纳法可知,插入排序的总时间与逆序对数量成正比。归并时,如果有交换前后子列元素,即A[1] 与A[2]两个子列归并时A[1]中元素被放置在A[2]中元素的后面,则A[2]中该元素以及A[1]中剩余所有元素放入逆序对中。最终归并完成时,可得到所有逆序对。伪代码:归并语句IfA[i]<A[j]后插一句

for(A[i]to A[last] in A[1]){Reverse_Pair.push(A[i],A[j]);}


 注:Reverse_Pair是全部储存逆序对的list。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值