编程之美(第3章 结构之法-字符串及链表的探索)总结

3.1 字符串移位包含的问题

问题:给定字符串s1和s2,判定s2是否能够被s1做循环移位得到的字符串包含。例如 s1 = AABCD,s2 = CDAA, 返回true,s1 = ABCD,s2 = ACBD,返回false。

方法一:对s1进行循环移位,遍历判断s2是否能被包含

方法二:观察规律,s1移位的结果是字符串的头移到字符串的结尾,如果把两个s1拼接起来,就包含了所有s1移位的字符串。因此再判断s2是否被两个s1拼接的字符串包含就行。

总结:利用空间来换取时间。即 提高空间复杂度来换取时间复杂度的降低。

3.2 电话号码对应英文单词

问题一:尽可能快地从这些字母组合中找到一个有意义的单词来表述一个电话号码。如用单词“computer”来描述号码26678837

问题二:对于一个电话号码,是否可以用一个单词来表示?

问题一方法一:直接循环法。
    将各个数字所能代表的字符存储在一个二维数组中,char c[10][10]
    将各个数字所能代表的字符总数记录在一个数组 int total[10] = {0, 0, 3, 3, ,3 ,3 ,3 , 4, 3, 4};
    用一个数组存储电话号码:int number[TelLength]
    将数字目前所代表的字符在其所能代表的字符集中的位置用一个数组存储起来: int answer[TelLength]

    通过两个while循环和一个for循环来遍历完所有的字母可能。

问题一方法二:递归的方法。
    通过改变第i位数字的索引来递归完成所有数字变化时的字母组合。

问题二方法一:利用问题一的解法,将电话号码对应的字符全部计算出来,然后去匹配字典,判断是否有答案。
问题二方法二: 如果查询次数多,直接将字典里面的单词按照转换规则转换为数字,存到文件中,使之成为一本数字字典。然后通过电话号码查表的方式来得到结果。

3.3 计算字符串的相似度

定义了一套方法把两个不相同的字符串变得相同,具体操作为:
  1. 修改一个字符;
  2. 增加一个字符;
  3. 删除一个字符。
把操作需要的次数定义为两个字符串的距离,而相似度等于“距离+1”的倒数。

问题:给定任意两个字符串,是否能够写出一个算法来计算出它们的相似度?

分析与解法:考虑如何把这个问题转化为规模较小的同样的问题。如果有两个串A=xabcdae和B=xfdfa,它们的第一个字符是相同的,只要计算A[2,...,7]= abcdae和B[2,...,5]=fdfa的距离就可以了。但是如果两个串的第一个字符不相同,那么可以进行如下的操作:
  1. 删除A串的第一个字符,然后计算A[2,...,lenA]和B[1,...,lenB]的距离。
  2. 删除B串的第一个字符,然后计算A[1,...,lenA]和B[2,...,lenB]的距离。
  3. 修改A串的第一个字符为B串的第一个字符,然后计算A[2,...,lenA]和B[2,...,lenB]的距离。
  4. 修改B串的第一个为A串的第一个字符,然后计算A[2,...,lenA]和B[2,...,lenB]的距离。
  5. 增加B串的第一个字符到A串的第一个字符之前,然后计算A[1,...,lenA]和B[2,...,lenB]的距离。
  6. 增加A串的第一个字符到B串的第一个字符之前,然后计算A[2,...,lenA]和B[1,...,lenB]的距离。

可以将上面6个操作合并为:
  1. 一步操作之后,再将A[2,...,lenA]和B[1,...,lenB]变成相同字符串;
  2. 一步操作之后,再将A[1,...,lenA]和B[2,...,lenB]变成相同字符串;
  3. 一步操作之后,再将A[1,...,lenA]和B[2,...,lenB]变成相同字符串。

可以利用递归来完成计算。

3.4 从无头单链表中删除节点

问题:假如有一个没有头指针的单链表。一个指针指向此单链表中间的 一个节点(不是第一个,也不是最后一个节点),请将该节点从单链表中删除。

解法:可以将该节点的下一节点的data赋值给当前节点,然后将当前节点的next指向下一节点的next,看似把下一节点给删除,但已经把下一节点的data赋值给此节点。实则是删除的此节点。  算法版的狸猫换太子。

3.5 最短摘要的生成

假设给定的已经是经过网页分词之后的结果,语句序列数组为W。其中W[0],W[1],...,W[N]为一些已经分好的词。
假设用户输入的搜索关键词为数组Q。其中Q[0], Q[1], ..., Q[m]为所有输入的搜索关键词。
这样,,生成的最短摘要实际上就是一串相互联系的分词序列。比如从W[i]到W[j], 其中,0<i<j<=N。

这些最短摘要是怎样生成的呢?

解法一:
  1. 从W数组的第一个位置开始查找出一段包含所有关键词数组Q的序列。计算当前的最短长度,并更新Seq数组。
  2. 对目标数组W进行遍历,从第二个位置开始,重新查找包含所有关键词数组Q的序列,同样计算出其最短长度,以及更新包含所有关键词的序列Seq,然后求出最短距离。
  3. 以此操作下去,一直到遍历至目标数组W的最后一个位置为止。
算法的复杂度为:O(N²*M)

解法二:
通过观察两次扫描的结果。第二次扫描从第一次扫描结束的地方开始。使之变成字符串匹配的问题。

3.6 编程判断两个链表是否相交

给出两个单向链表的头指针,比如h1、h2,判断这两个链表是否相交,为了简化问题,假设两个链表均不带环。

解法一:直观的想法
直接判断第一个链表的每个节点是否在第二个链表中。这种方法的时间复杂度为O(Length(h1)*Length(h2))。

解法二:利用计数的方法
如果两个链表相交,那么这两个链表就会有共同的节点。而节点地址又是节点的唯一标识。所有通过能够判断两个链表中是否存在一致的节点,就可以知道这两个链表是否相交。
一个简单的办法就是对第一个链表的节点地址进行hash排序,建立hash表,然后针对第二个链表的每个节点的地址查询hash表,如果它在hash表中出现,那么说明第二个链表和第一个链表有共同的节点。此方法的时间复杂度为O(Length(h1) + Length(h2))。但是要同时附加O(Length(h1))的存储空间来存储哈希表。

解法三:转化为另一已知问题
由于两个链表都没有环,可以把第二个链表接在第一个链表后面,如果得到的链表有环,则说明这两个链表相交。否则,这两个链表不相交。这样就可以把问题转化为判断一个链表是否有环。
需要注意的是: 在这里 如果有环,则第二个链表的表头一定在环上,只需要从第二个链表开始遍历,看是否会回到起始点就可以判断出来。

解法四:抓住要点

抓住“如果两个没有环的链表相较于某一节点,那么在这个节点之后的所有节点都是两个链表共有的”。

先遍历第一个链表,记住最后一个节点。然后遍历第二个链表,到最后一个节点时和第一个链表的最后一个节点做比较,如果相同,则相交,否则,不相交。
时间复杂度为O(Length(h1) + Length(h2)), 而且只用了一个额外的指针来存储最后一个节点。

3.7 队列中取最大值操作问题

假设有这样一个拥有3个操作的队列:
  1. EnQueue(v): 将v加入队列中
  2. DeQueue: 使队列中的队首元素删除并返回此元素
  3. MaxElement:返回队列中的最大元素

设计一种数据结构和算法,让MaxElement操作的时间复杂度尽可能地低。

解法一:这个问题的关键在于取最大值的操作,并且得考虑当队列里面的元素动态增加和减少的时候,如何能够非常快速地把最大值取出。

最直接的思路是按照传统方式来实现队列:利用一个数组或链表来存储队列的元素,利用两个指针分别指向队列的队首和队尾。采用这种方式,那么MaxElement操作需要遍历队列的所有元素。在队列的长度为N的条件下,时间复杂度为O(N)。

解法二:
考虑用最大堆来维护队列中的元素。堆中每个元素都有指针指向它的后续元素。这样,堆就可以很快实现返回最大元素的操作。同时,也能保证队列的正常插入和删除。MaxElement操作其实就是维护一个最大堆,其时间复杂度为O(1)。而入队和出队操作的时间复杂度为O(log2N)。

解法三:
解法二比解法一快是因为它利用一个指针集合保持了队列中元素的相对大小关系。所以返回最大值只需要O(1)的时间复杂度,而在元素入队或出队时,更新这个指针集合都需要O(log2N)的时间复杂度。所以给我们提供一个思路就是去寻找一种新的保存队列中元素相当大小关系的指针集合,并且使得更新这个指针集合的时间复杂度更低。

对于栈来讲,Push和Pop操作都是在栈顶完成的,所以很容易维护栈中的最大值,它的时间复杂度为O(1)。

class stack
{
public:
    stack()
    {
        stackTop = -1;
        maxStackItemIndex = -1;
    }    
    void Push(Type x)
    {
        stackTop++;
        if(stackTop >= MAXN)
            ;    //超出栈的最大存储量
        else
        {
            stackItem[stackTop] = x;
            if(x > Max())
            {
                link2NextMaxItem[stackTop] = maxStackItemIndex;
                maxStackItemIndex = stackTop;
            }
            else
                link2NextMaxItem[stackTop] = -1;
        }
    }
    Type Pop()
    {
        Type ret;
        if(stackTop < 0)
            ThrowException();    //已经没有元素了,所以不能pop
        else
        {
            ret = stackItem[stackTop];
            if(stackTop == maaxStackItemIndex)
            {
                maxStackItemIndex = link2NextMaxItem[stackTop];
            }
            stackTop--;
        }
        return ret;
    }
    Type Max()
    {
        if(maxStackItemIndex >= 0)
            return stackItem(maxStackItemIndex);
        else
            return -INF;
    }
private:
    Type stackItem[MAXN];
    int stackTop;
    int link2NextMaxItem[MAXN];
    int maxStackItemIndex;
}

维护一个最大值的序列(link2NextMaxItem)来保证Max操作的时间复杂度为O(1),相当于用空间复杂度换取了时间复杂度。

3.8 求二叉树中节点的最大距离

问题:如果把二叉树看成一个图,父子节点之间的连线看成是双向的,我们姑且定义“距离” 为两个节点之间边的个数。  写一个程序求一棵二叉树中相距最远的两个节点之间的距离。

解法一:根据相距最远的两个节点一定是叶子节点这个规律,可以发现:
对于任意一个节点,以该节点为根,假设这个根有k个孩子节点,那么相距最远的两个节点U和V之间的路径与这个根节点的关系有两种情况。
  1. 若路径经过根Root,则U和V是属于不同子树的,且它们都是该子树中到根节点最远 的节点,否则跟它们的距离最远相矛盾。
  2. 如果路径不经过Root,那么它们一定属于根的k个子树之一。并且它们也是该子树中相距最远的两个顶点。

因此,问题就可以转化为在子树上的解,从而利用动态规划来解决。
设第k棵子树中相距最远的两个节点:Uk和Vk,其距离定义为d(Uk,Vk),那么节点Uk或Vk即为子树K到根节点Rk距离最长的节点。设Uk为子树K中到根节点Rk距离最长的节点,其到根节点的距离定义为d(Uk,R)。取d(Ui,R)(1≤i≤k)中最大的两个值max1和max2,那么经过根节点R的最长路径为max1+max2+2,所以树R中相距最远的两个点的距离为:max{d(U1,V1),..., d(Uk, Vk ),max1+max2+2}。
采用深度优先搜索,只需要遍历所有的节点一次,时间复杂度为O(|E|) = O(|V| - 1),其中V为点的集合,E为边的集合。

代码如下,使用二叉树来实现:
//数据结构定义
struct NODE
{
    NODE* pLeft;    //左子树
    NODE* pRight;    //右子树
    int nMaxRight;    //左子树中的最长距离
    int nMaxRight;    //右子树中的最长距离
    char chValue;    //该节点的值
};

int nMaxLen = 0;

//寻找树中最长的两段距离
void FindMaxLen(NODE* pRoot)
{
    //遍历到叶子节点,返回
    if(pRoot == NULL)
    {
        return;
    }

    //如果左子树为空,那么该节点的左边最长距离为0
    if(pRoot -> pLeft == NULL)
    {
        pRoot -> nMaxLeft = 0;
    }

    //如果右子树为空,那么该节点的右边最长距离为0
    if(pRoot -> pRight == NULL)
    {
        pRoot -> nMaxRight = 0;
    }

    //如果左子树不为空,递归寻找左子树最长距离
    if(pRoot -> pLeft != NULL)
    {
        FindMaxLen(pRoot -> pLeft);
    }

    //如果右子树不为空,递归寻找右子树最长距离
    if(pRoot -> pRight != NULL)
    {
        FindMaxLen(pRoot -> pRight);
    }

    //计算左子树最长节点距离
    if(pRoot -> pLeft != NULL)
    {
        int nTempMax = 0;
        if(pRoot -> pLeft -> nMaxLeft > pRoot -> pLeft -> nMaxRight)
        {
            nTempMax = pRoot -> pLeft -> nMaxLeft;
        }
        else
        {
            nTempMax = pRoot -> pLeft -> nMaxRight;
        }
        pRoot -> nMaxLeft = nTempMax + 1;
    }

    //计算右子树最长节点距离
    if(pRoot -> pRight != NULL)
    {
        int nTempMax = 0;
        if(pRoot -> pRight -> nMaxLeft > pRoot -> pRight -> nMaxRight)
        {
            nTemp = pRoot -> pRight -> nMaxLeft;
        }
        else
        {
            nTemp = pRoot -> pRight -> nMaxRight;
        }
        pRoot -> nMaxRight = nTempMax + 1;
    }

    //更新最长距离
    if(pRoot -> nMaxLeft + pRoot -> nMaxRight > nMaxLen)
    {
        nMaxLen = pRoot -> nMaxLeft + pRoot -> nMaxRight;
    }
}


分析递归问题,要遵循以下:
  1. 先弄清楚递归的顺序。在递归的实现中,往往须要假设后续的调用已经完成,在此基础上,才实现递归的逻辑。在该题中,就是假设已经把后面的长度计算出来了,然后继续考虑后面的逻辑。
  2. 分析清楚递归体的逻辑,然后写出来。比如在上面的问题中,递归体的逻辑就是如何计算两边最长的距离。
  3. 考虑清楚递归退出的边界问题。也就是说,哪个地方写return。


3.9 重建二叉树

二叉树的三种遍历次序--前序、中序、后序。如果知道了遍历的结果,能不能把一棵二叉树重新构造出来呢?

给定一棵二叉树,假设每个节点都用唯一的字符来表示,具体结构如下:

struct NODE{
    NODE* pLeft;
    NODE* pRight;
    char chValue;    //也可以是其它数据类型
};

假设已经有了前序遍历和中序遍历的结果,希望通过一个算法重建这棵树。
给定函数的定义如下:

void Rebuild (char* pPreOrder, char* pInOrder, int nTreeLen, NODE** pRoot)

参数:
pPreOrder: 以null为结尾的前序遍历结果的字符串数组。
pInOrder: 以null为结尾的中序遍历结果的字符串数组。
nTreeLen:树的长度。
pRoot:返回node**类型,根据前序和中序遍历结果重新构建树的根节点。

分析:
前序遍历:先访问当前节点,然后以前序访问左子树,右子树。
中序遍历:先以中序遍历左子树,接着访问当前节点,然后以中序遍历右子树。

前序遍历的每一个节点,都是当前子树的根节点。同时,以对应的节点为边界,就会把中序遍历的结果分为左子树和右子树。

代码如下:
//ReBuild.cpp:根据前序及中序结果,重建树的根节点

//定义树的长度,为了后序调用实现的简单,我们直接用宏定义了树节点的总数
#define TREELEN 6

//树节点
struct NODE
{
    NODE* pLeft;    //左节点
    NODE* pRight;    //右节点
    char chValue;    //节点值
};

void ReBuild(char* pPreOrder,    //前序遍历结果
            char* pInOrder,      //中序遍历结果
            int nTreeLen,        //树长度
            NODE** pRoot)        //根节点
{
    //检查边界条件
    if(pPreOrder == NULL || pInOrder == NULL)
    {
        return;
    }

    //获得前序遍历的第一个节点
    NODE* pTemp = new NODE;
    pTemp -> chValue = *pPreOrder;
    pTemp -> pLeft = NULL;
    pTemp -> pRight = NULL;

    //如果节点为空,把当前节点复制到根节点
    if(*pRoot == NULL)
    {
        *pRoot = pTemp;
    }

    //如果当前树长度为1,那么已经是最后一个节点
    if(nTreeLen == 1)
    {
        return;
    }

    //寻找子树的长度
    char* pOrgInOrder = pInOrder;
    char* pLeftEnd = pInOrder;
    int nTempLen = 0;

    //找到左子树的结尾
    while(*pPreOrder != pLeftEnd)
    {
        if(pPreOrder == NULL || pLeftEnd == NULL)
        {
            return;    
        }

        nTempLen++;

        //记录临时长度,以免溢出
        if(nTempLen > nTreeLen)
        {
            break;
        }
        pLeftEnd++;
    }

    //寻找左子树长度
    int nLeftLen = 0;
    nLeftLen = (int) (pLeftEnd - pOrgInOrder);

    //寻找右子树长度
    int nRightLen = 0;
    nRightLen = nTreeLen - nLeftLen - 1;

    //重建左子树
    if(nLeftLen > 0)
    {
        ReBuild(pPreOrder+1, pInOrder, nLeftLen, &((*pRoot) -> pLeft));
    }

    //重建右子树
    if(nRightLen > 0)
    {
        Rebuild(pPreOrder + nLeftLen + 1, pInOrder + nLeftLen + 1, nRightLen, &((*pRoot) -> pRight));
    }
}

//示例的调用代码
int main(int argc, char* argv[])
{
    char szPreOrder[TREELEN] = {'a', 'b', 'd', 'c', 'e', 'f};
     char  szInOrder[TREELEN] = {'d',  'b', 'a', 'e', 'c', 'f};

       NODE* pRoot = NULL;
      ReBuild(szPreOrder, szInOrder, TREELEN, &pRoot);
}

3.10 分层二叉树

问题一:给定一棵二叉树,要求按分层遍历该二叉树,即从上到下按层次访问该二叉树(每一层将单独输出一行),每一层要求访问的顺序为从左到右,并将节点依次编号。

问题二:写另外一个函数,打印二叉树中某层次的节点(从左到右),其中根节点为第0层,函数原型为int PrintNodeAtLevel(Node* root, int level),成功返回1,失败则返回0。

分析与解法:
定义节点的数据结构为(该二叉树中的数据类型为整数):
struct Node
{
    int data;    //节点中的数据
    Node* lChild;    //左子指针
    Node* rChild;    //右子指针
}

假设要求访问二叉树中第k层的节点,那么其实 可以把它转换成分别访问“以该二叉树根节点的左右子节点为根节点的两颗子树”中层次为k-1的节点。
//输出以root为根节点中的第level层中的所有节点(从左到右)
//失败则返回0
//root为二叉树的根节点
//level为层次数,其中根节点为第0层
int PrintNodeAtLevel(Node* root, int level)
{
    if(!root || level < 0)
        return 0;
    if(level == 0)
    {
        cout << root -> data << "";
        return 1;
    }
    return PrintNodeAtLevel(node -> lChild, level - 1) + PrintNodeAtLevel(node -> rChild, level - 1);
}

采用递归算法,思路清晰,缺点是递归函数的调用效率较低,无论是耗费的计算时间还是占用的存储空间都比非递归算法要多。

问题一的解法只需要知道二叉树的深度n,调用n次PrintNodeAtLevel()
//层次遍历二叉树
//root,二叉树的节点
//depth,树的深度
void PrintNodeByLevel(Node* root, int depth)
{
    for(int level = 0; level < depth; level++)
    {
        PrintNodeAtLevel(root, level);
        cout << endl;
    }
}


在访问第k层的时候,只需要知道第k-1层的节点信息就足够了,所以在访问第k层的时候,要是能够知道第k-1层的节点信息,就不再需要从根节点开始遍历了。

可以从根节点出发,依次将每层的节点从左到右压入一个数组,并用一个游标Cur记录当前访问的节点,另一个游标Last指示当前层次的最后一个节点的下一个位置,以Cur == Last作为当前层次访问结束的条件,在访问某一层的同时将该层的所有节点的子节点压入数组,在访问完某一层之后,检查是否还有新的层次可以访问,直到访问完所有的层次。

//按层次遍历二叉树
//root,二叉树的根节点
void PrintNodeByLevel(Node* root)
{
    if(root == NULL)
        return;
    vector<Node*> vec;    
    vec.push_back(root);
    int cur = 0;
    int last = 1;
    while(cur < vec.size())
    {
        Last = vec.size();    //新的一行访问开始,重新定位last于当前行最后一个节点的下一个位置
        while(cur < last)
        {
            cout << vec[cur] -> data << " ";     //访问节点
            if(vec[cur] -> lChild)    //当前访问节点的左节点不为空则压入
                vec.push_back(vec[cur]->lChild);
                    if(vec[cur] -> rChild)     //当前访问节点的右节点不为空则压入
                   vec.push_back(vec[cur]->rChild);
            cur++;
        }
        cout << endl;    //当cur == last时,说明该层次访问结束,输出换行符
    }
}






 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值