lua 算法题集(1)

此处收集一些简单的算法以及其lua的简单实现。(题目来自于剑指offer、程序员代码面试指南、网络资源等;提供的实现代码并非为最优或最佳,也可能遗漏了某些特殊情形,因此仅供参考。若存在问题,可以留言交流。)

1 数据结构相关

1.1 数组

1 将一个递增数组前后部分互换,查找数组的最小数

local t = {3,4,5,12,45,1.1,1.5,2} -- 一般情形
local t = {1,0,1,1,1} -- 特殊情形 只能走顺序查找

function getPivot( )
    local low = 1
    local high = #t
    -- 基本有序 采用二分查找
    -- low指向前半部分 high指向后半部分  high - low <= 1时high位置即为查找位置
    while low <= high do
        local mididx = math.floor((low+high)/2)
        if high - low <= 1 then
            return t[high]
        end
        if t[low] == t[mididx] and t[high] == t[mididx] then
            -- 此种情形 必须顺序遍历查找了
            return minByOrder(t,low,high)
        end
        if t[low] <= t[mididx] then
            -- 前半部分 low前进
            low = mididx
        else 
            -- 后半部分 high后退
            high = mididx
        end
    end
end
function minByOrder( t,low,high )
    -- 首次降低 即为最小
    for i=low,high do
        if t[i] > t[i+1] then
            return t[i+1]
        end     
    end
end
-- test
print(getPivot())

2 打印1到最大的n位数

如果n过大(如100)无论数据定位何种类型都有可能溢出。对于大数问题,可采用字符串数组表达。

function addOneToStr(str)
    local up = 1
    for i=#str,1,-1 do
        str[i] = str[i] + up
        if str[i] > 9 then
            -- 进位
            str[i] = 0
            if i == 1 then
                -- 全部后移一位
                for i=#str,1,-1 do
                    str[i+1] = str[i]
                end
                str[1] = 1
                break
            end
        else
            -- 无需进位
            break
        end
    end
end

-- test
n = 3
str = {0}
for i=0,math.pow(10,n)-1 do
    if n == 0 then
        return
    end
    local result = ""
    for i=1,#str do
        result = result..str[i]
    end
    print(result)
    -- 基于数组的加1操作
    addOneToStr(str)
end

上述代码虽直观,但由于需要模拟加法,稍微麻烦,还有一种更简洁的做法,利用全排列递归方式实现。

function recurse(a,index)
    if index > #a then
        printT(a)
        return
    end
    for i=0,9 do
        a[index] = i
        recurse(a,index+1)
    end
end
function printT( a )
    local st = ""
    local firstValid = false
    for i=1,#a do
        -- 前置0忽略
        if a[i] == 0 and not firstValid then
            -- 全部为0
            if i == #a then
                st = "0"
            end
        else
            st = st..a[i]
            firstValid = true
        end
    end
    print(st)
end
-- test
a = {0,0,0}
recurse(a,1)

3 输出字符串的所有排列,如abc的所有输出:abc acb bac bca cba cab

用递归很容易实现,但有两个注意点:1)借助于字符交换实现O(1)的空间复杂度;2)递归后一定要将数组重新交换已还原递归前的数组状态,因为递归中始终使用的是同一个数组,后续的变动将引起前期的数组变化。

local t = {"a","b","c"}
local len = #t
function digui( srcT,destT,index )
    if index == len+1 then
        printT(destT)
        return
    end

    for i = index,len do
        destT[index] = srcT[i]

        -- 交换index与i位置值  
        local tmp = srcT[index]
        srcT[index] = srcT[i]
        srcT[i] = tmp

        digui(srcT,destT,index+1)

        -- 交换位置 复原未递归前的状态(重要)
        local tmp = srcT[index]
        srcT[index] = srcT[i]
        srcT[i] = tmp
    end
end

function printT( t )
    local st = ""
    for i,v in ipairs(t) do
        st = st .. v
    end
    print(st)
end
-- test
digui(t,{},1)

4 求字符的所有无序组合,如abc的所有组合为a b c ab ac bc abc。

该问题仍可通过递归求解,不同的是,对于组合而言,ab与ba是同一个。对于一个字符数组,每个位置上的字符要不参与组合,要不不参与组合。针对当前位置的两个状态,进行后续位置的递归操作。

local t = {"a","b","c","d","e"}
local len = #t

-- tIndex:当前递归进程中遍历到的原始数组的位置
-- resultRemainNum:还需组合的剩余字符总数 | resultTotalNum:字符总数
function digui(result,tIndex,resultRemainNum,resultTotalNum)
    -- 组合完成 
    if resultRemainNum <= 0 then
        printT(result)
        return
    end

    -- 组合的剩余字符总数与原表中剩余字符总数相等 无需递归 全部复制即可
    -- 如组合还剩余两个位置 此时index刚好走到"d" 直接将t表中后面的字符复制给result
    if resultRemainNum == #t-tIndex+1 then
        for i=tIndex,#t do
            result[resultTotalNum-resultRemainNum+1+(i-tIndex)] = t[i]
        end
        printT(result)
        return
    end

    -- 需要组合字符数大于原数组能提供的字符数  异常情形
    if resultRemainNum > #t-tIndex+1 then
        return
    end

    -- 访问到index时 组合有两种行为:使用该字符|不使用该字符 
    -- 如果组合使用了该字符,继续向后遍历原数组,并且剩余需要组合的字符减1
    result[resultTotalNum-resultRemainNum+1] = t[tIndex]
    digui(result,tIndex+1,resultRemainNum-1,resultTotalNum)
    -- 如果组合未使用该字符,剩余需要组合的字符总数不变
    result[resultTotalNum-resultRemainNum+1] = nil
    digui(result,tIndex+1,resultRemainNum,resultTotalNum)
end

local st = ""
function printT( t )
    st = st.."  "
    for i,v in ipairs(t) do
        st = st .. v
    end
end
-- test
for i=1,#t do
    digui({},1,i,i)
end

print(st)

5 连续子数组的最大和。数组中有正负值,提取和最大的子数组。如对于数组{1,-2,3,10,-4,7,2,-5},和最大的子数组为{3,10,-4,7,2}。

local t = {1,-2,3,10,-4,7,2,-5}
function getSubMaxArray()
    local startIdx,endIdx,curStart = 1,1,1
    local tmp = 0
    local max = 0
    for i=1,#t do
        if tmp < 0 then
            -- 和变为负值, 重新开始计算子数组和(负值必会使得和变小 因此要抛弃重新开始)
            curStart = i -- 新的起点
            tmp = t[i] -- 新的子数组和
        else
            tmp = tmp + t[i] -- 和累加
        end

        if tmp > max then
            -- 当前子数组和超过历史记录 更新子数组起止点 
            max = tmp
            startIdx = curStart
            endIdx = i
        end
    end
    print(max,startIdx,endIdx)
end
-- test
getSubMaxArray()

6 统计从1到n整数中1出现过的次数

 这种抽象问题,最好通过实例来寻找规律。以n=21345为例,首先将整数分为两段:1-1345、1346-21345。先来考虑1346-21345,首位(first)为2,那么首位为1时,从千位到个位,0-9数字随机出现,因此出现1的总数(firstNum)为:10^4,如果首位等于1呢,如11345?那么出现1的总数因为:1345+1。
 我们再将1346-21345分为两段:1346-11345、11346-21345。对于1346-11345来说,假定千位为1,那么从百位到个位数字在0-9随机出现,总数为10^3,总共四位(len-1),因此总数为4*(10^3),11346-21345计算方式一样,因此1346-21345出现1的总数为:2*4*(10^3)。因此,仅考虑首位,1出现的总数为:10^4+2*4*(10^3)。用公式表达为:firstNum + first*(len-1)*(10^(len-2))。那么对于1-1345,如何统计?实际上这是一个递归的过程,我们完成了首位的统计,后续的就是逐位递归。

local n = 21345
function digui(n)
    if n < 1 then
        return 0
    end
    local first,len = getFirstLen(n)
    -- 仅一位数 特殊处理
    if len == 1 then
        return 1
    end

    local firstNum = 0
    -- 先统计首位出现的1总数
    if first == 1 then
        firstNum = n - math.pow(10,len-1) +1
    elseif first > 1 then
        firstNum = math.pow(10,len-1)
    end

    -- 除首位外,某位定位1,其他位0-9随机
    firstNum = firstNum + first*(len-1)*math.pow(10,len-2)
    -- 去除首位 继续递归
    n = n - first*math.pow(10,len-1)
    return firstNum + digui(n)
end

function getFirstLen( n )
    local len = 1
    while n > 10 do
        len = len+1
        n = math.floor(n/10)
    end
    return n,len
end
-- test
print(digui(n))

7 寻找n个丑数

因子仅包含2、3、5的数成为丑数。寻找第n个丑数。这个丑数实际上是在已找到的丑数数组中乘以上述基础因子,来逐步增加丑数数目,因此我们在计算时要保存已计算好的丑数;同时,在某个时刻,基础因子总与丑数数组的每个数对应(因为一个新的丑数总是来源于旧的丑数乘以基础因子),因此需要用一个数组保存因子对应的丑数数组的位置。

local factorT = {2,3,5}
function getUgly(n)
    local uglyT = {1} -- 存放已发现的丑数
    local factorInUglyIdx = {1,1,1} -- 记录基础因子对应uglyT表的位置
    local curUgly = 1 -- 记录当前丑数
    local i = 1
    while i < n do
        local min = nil
        local minK = -1
        for k,v in ipairs(factorT) do
            -- 与基础因子相乘,寻找最小值作为新的丑数
            local tmp = v*uglyT[factorInUglyIdx[k]]
            if not min or tmp <= min then
                minK = k
                min = tmp
            end
        end

        if(curUgly == min) then
            -- 找到的丑数与前一次相同  不做处理
        else
            -- 找到一个新的丑数
            curUgly = min
            table.insert(uglyT,min)
            i = i+1
        end
        -- 在已有丑数表中的位置进1
        factorInUglyIdx[minK] = factorInUglyIdx[minK]+1
    end
    print(curUgly)
end
-- test
getUgly(20)

8 数组中的逆序对

 在{4,7,5,6}中,逆序对为{7,5}、{7,6}、{5、6}。如果用两层for循环,复杂度O(n^2)。如果我们以1/2的方式缩小数组规模(lg(n)),然后在小规模小统计逆序对,然后合并小规模保持其有序,然后逐层递归,复杂度能达到O(nlg(n))。这实际上就是一个归并排序的过程。
 在下面程序中有一个小技巧:归并排序在递归完需要利用一个临时数组更新并合并已经分别保持有序的左右子数组,数组更新不可避免,那么该合并过程?我们可通过增加一个辅助数组。用该辅助数组参与数组的更新,完成当前递归后,将辅助数据直接作为原数组,将原数组作为辅助数组进行后续计算。

local t = {7,5,6,4,1,9,11,3}
local copy = {7,5,6,4,1,9,11,3}

function merge( t,copy,startIdx,endIdx )
    if endIdx - startIdx <= 0 then
        return 0
    end

    local midIdx = math.floor((startIdx+endIdx)/2)

    -- 递归 统计左右子数组的逆序对
    local reversedNumInLeft = merge(copy,t,startIdx,midIdx) -- 将辅助数组copy作为原数组,将原数组作为辅助数组进行后续计算
    local reversedNumInRight = merge(copy,t,midIdx+1,endIdx)

    -- 递归后 左右子数组保持有序 将其合并
    local reversedNumInMerge = 0
    local tmpIdx = endIdx
    local preIdx,postIdx = midIdx,endIdx

-- [[ 第一种形式 
    while startIdx <= preIdx or midIdx+1 <= postIdx do -- 左侧循环
        while startIdx <= preIdx do
            if midIdx+1 > postIdx then
                -- 右侧子数组已全部处理完成 更新至辅助数组copy
                copy[tmpIdx] = t[preIdx]
                preIdx = preIdx - 1
                tmpIdx = tmpIdx - 1
            else
                if t[preIdx] <= t[postIdx] then
                    -- 退出当前循环体(当前循环体仅用于处理左侧子数组 右侧子数组的处理交予后面的循环体)
                    break
                else
                    -- 当前左侧数大于右侧数 表明存在逆序对
                    copy[tmpIdx] = t[preIdx] -- 更新辅助数组
                    preIdx = preIdx - 1
                    tmpIdx = tmpIdx - 1
                    -- 统计逆序对 
                    -- 什么原理?举例说明:左侧子数组{4,7},右侧{5,6},当处理左侧的7时,postIdx指向的6小于7,共有两个逆序对(7,5)与(7,6)
                    reversedNumInMerge = reversedNumInMerge + postIdx-(midIdx+1)+1
                end
            end
        end
        while midIdx+1 <= postIdx do -- 右侧循环
            if startIdx > preIdx then 
                -- 左侧子数组全部处理完成
                copy[tmpIdx] = t[postIdx]
                postIdx = postIdx - 1
                tmpIdx = tmpIdx - 1
            else
                if t[preIdx] > t[postIdx] then
                    -- 交予左侧循环处理
                    break
                else
                    copy[tmpIdx] = t[postIdx]
                    postIdx = postIdx - 1
                    tmpIdx = tmpIdx - 1
                end
            end
        end
    end
--]]

--[[ 第二种形式 简洁一些但效率不变
    while startIdx <= preIdx and midIdx+1 <= postIdx do -- 左侧循环
        if t[preIdx] > t[postIdx] then
            copy[tmpIdx] = t[preIdx]
            preIdx = preIdx - 1
            tmpIdx = tmpIdx - 1
            reversedNumInMerge = reversedNumInMerge + postIdx-(midIdx+1)+1
        else
            copy[tmpIdx] = t[postIdx]
            postIdx = postIdx - 1
            tmpIdx = tmpIdx - 1
        end
    end
    for i=preIdx,startIdx,-1 do
        copy[tmpIdx] = t[i]
        tmpIdx = tmpIdx - 1
    end
    for i=postIdx,midIdx+1,-1 do 
        copy[tmpIdx] = t[i]
        tmpIdx = tmpIdx - 1
    end
--]]

    return reversedNumInLeft+reversedNumInRight+reversedNumInMerge
end

--test
local rt = merge(t,copy,1,#t)

9 打印和为某整数的连续正数序列

 过程比较简单。设立前后指针,小于目标整数时后指针向后走,大于目标整数时前指针向后走,两指针相遇时退出。

function f(target)
    local pre = 1
    local post = 2
    local sum = pre + post
    while pre < post do -- 指针相遇 退出
        while sum <= target do
            if sum == target then
                printSeq(pre,post)
            end
            -- 小于目标 后指针向后走
            post = post + 1
            sum = sum + post
        end
        while sum > target do
            -- 大于目标 前指针向后走
            sum = sum - pre
            pre = pre + 1
        end
    end
end
function printSeq(pre,post)
    for i=pre,post do
        print(i)
    end
end
-- test
f(15) 

1.2 堆栈

1 栈逆序 (仅能用递归 不能使用辅助空间)

不能使用辅助空间,意味着只能对原栈进行操作以完成逆序。需要两个递归函数。
第一个递归函数:移除栈底元素并返回。第二个递归:递归调用第一个函数,之后逐个压入栈内。

-- 第一个递归函数
-- 返回最后一个 其余重新压入
function removeLastEle()
    local curEle = stack:pop()
    if stack:isEmpty() then
        -- curEle是最后一个元素
        return curEle
    end
    local lastEle = self:removeLastEle()
    stack:push(curEle)
    return lastEle -- 始终返回最后一个元素
end
-- 第二个递归函数
function revertStackEle()
    if stack:isEmpty() then
        return
    end
    local curEle = removeLastEle() -- 逐个获取栈底 
    revertStackEle()
    stack:push(curEle) -- 相当于倒序压入
end

2 生成窗口最大值数组

一个长度为n的整型数组和一个大小为w的窗口从数组的最左边滑到最右边。输出这n-w+1个窗口的最大值。
暴力解法为每生成一个窗口遍历一遍窗体并计算最大值,复杂度O(n*w)。可以用双端队列将复杂度将至O(n)。队列头存放数组最大值位置i,当其失效(移动窗口离开该位置)则弹出;当数组中的值大于队列尾部值时,从队列尾部弹出,然后将该值压入队列。

local winSize = 3
 local arr = {4,3,5,4,3,3,6,7}
 function findMaxInWin()
    for i=1,#arr do
        if queueMax:isEmpty() then
            -- 如果双向队列为空 入队尾
            queueMax:push_back(i)
        else
            if i - queueMax:front() >= 3 then
                -- 队头无效 弹出
                queueMax:pop_front()
            end 
            while not queueMax:isEmpty() and arr[queueMax:back()] <= arr[i] do
                -- 弹出所有小于arr[i]的数
                queueMax:pop_back()
            end
            queueMax:push_back(i)
        end
        if i >= winSize then
            -- 输出当前窗体最大值
            print(arr[i])
        end
    end
 end

3 构造数组(无重复)的MaxTree

MaxTree定义为:二叉树;树中的任何子树,最大的节点均为树头。
构造方法:1)每个数父节点为它左侧第一个(从它向左侧数起)比它大的数和它右侧第一个比它大的数中的较小值;2)整个数组的最大值为MaxTree的头节点。该方法能得到有效二叉树。反证:如果一个父节点n有两个以上孩子节点,则必有两个数i,j在n的同侧,这是矛盾的。因为如果i < j < n,那么i的父节点应当为j;如果j < i < n,那么j的父节点应该为i。

local a = {3,4,5,1,2}
function generateTree()
    local nodeLst = {}
    for i,v in ipairs(a) do
        local curNode = createNode(v)
        table.insert(nodeLst,curNode)
    end

    local leftBigMap = {} -- 记录节点左侧第一个大于它的节点
    for i=1,#a do
        while not stack:isEmpty() and stack:peak() <= a[i] do -- 这种构造式能够让栈保持递减序列
            -- a[i]大于栈顶 递减序列会被破坏 因此需将所有小的数弹出
            -- 同时,弹出时将前一个数记录为后一个数的leftBigNode(递减序列保证了这一点)
            MapAndPop(stack,leftBigMap)
        end
        stack:push(a[i])
    end
    while not stack:isEmpty() do
        -- 处理栈中未处理数据
        MapAndPop(stack,leftBigMap)
    end

    local rightBigMap = {} -- 记录节点右侧第一个大于它的节点
    for i=1,#a do
        while not stack:isEmpty() and stack:peak() <= a[i] do
            MapAndPop(stack,rightBigMap)
        end
        stack:push(a[i])
    end
    while not stack:isEmpty() do
        MapAndPop(stack,rightBigMap)
    end

    local headNode -- 树头
    for k,v in pairs(nodeLst) do
        -- 构建二叉树
        local leftBigNode = leftBigMap[v]
        local rightBigNode = rightBigMap[v]
        if not leftBigNode and not leftBigNode then
            headNode = v
        elseif not leftBigNode and rightBigNode then
            rightBigNode.leftNode = v
        elseif leftBigNode and not rightBigNode then
            leftBigNode.rightNode = v
        elseif leftBigNode and rightBigNode then
            if leftBigNode.value < rightBigNode.value then
                leftBigNode.rightNode = v
            else
                rightBigNode.leftNode = v
            end
        end
    end
    return headNode
end
function MapAndPop( stack,leftBigMap )
    local popNode = stack:pop()
    local peakNode = (not stack:isEmpty()) and stack:peak() or nil 
    leftBigMap[popNode] = peakNode
end

4 计算最大子矩阵大小

给定一个map矩阵,仅包含0,1,计算其中全是1的矩形区域中,面积最大的一个。
方法:1)计算每一层至第一层 每个列位置上1连续出现的次数;2)计算每一层能够生成的最大矩形面积。

local map = {
             [1] = {1,0,1,1},
             [2] = {1,1,1,1},
             [3] = {1,1,1,0},
            }
local height = {}
local MaxRectSize = 0
function MaxRectSize()
    -- 为每一层建立高度数组
    for i,#map do
        -- 统计从1层至i层 每个列位置出现连续1的总数
        height[i] = {}
        for j=1,#map[i] do
            height[i][j] = 0    
        end
        for j=1,#map[i] do
            if i == 1 then
                height[i][j] = map[i] > 0 and 1 or 0
            else
                height[i][j] = map[i] > 0 and height[i-1][j] + 1 or 0
            end
        end
    end

    -- 统计每一层的最大矩阵面积(其实就是全部为1的矩形内1的数目)
    for i=1,#height do
        -- 计算该层 某列左侧及右侧大于它高度的最远位置(实际上就是找出左侧及右侧第一个小于它高度的位置 再加减1)
        -- 我们仍使用栈完成这一搜索过程
        local stack = Stack:create()
        for j=1,#height[i] do
            while not stack:isEmpty() and stack:peak() >= height[i][j] do -- 该方式将栈构造成递增序列(当前高度小于等于栈顶 pop)
                popAndCalcuMaxRectSize(stack,height[i],j)
            end
            stack:push(j)
        end
        -- 如果栈内还有数据 继续出栈处理
        while not stack:isEmpty() do
            -- 栈内还有数据 表明其高度均小于最后一个位置的高度(否则在遍历最后一个元素时必将其pop) 
            -- 因此它的rightBiggerColIdx为#height[i]
            popAndCalcuMaxRectSize(stack,height[i],#height[i]) 
        end
    end
end
function popAndCalcuMaxRectSize( stack,curRowHeight,rightBiggerColIdx )
    local rightBiggerHeight = curRowHeight[rightBiggerColIdx]
    local popIdx = stack:pop()
    local popHeight = curRowHeight[popIdx]
    local peakIdx = -1
    if not stack:isEmpty() then
        peakIdx = stack:peak()
    end

    -- 弹出位置对应高度的左侧第一个小于它高度的位置是栈顶记录的位置
    -- 以 {3,4,5,4}为例 经过一次处理后,中间所有大于等于4的数4,5被pop,形成的新栈为{peakidx=3,popidx=4}
    -- 可以发现:posidx左侧第一个小于它的位置就是peakidx
    local leftFirstSmallIdx = peakIdx
    -- 弹出位置对应高度的右侧第一个小于它高度的位置正是将要入栈的位置rightBiggerColIdx
    -- 该位置若要被弹出  说明第一次遇到了小于它的数
    local rightFirstSmallIdx = rightBiggerColIdx
    if popHeight > rightBiggerHeight then
        local curMaxRectSize = ((rightBiggerColIdx-1)-(leftFirstSmallIdx+1)+1)*popHeight -- 将当前pop位置左右扩展至最远的大于它的高度 距离乘以高度即为完全包含1的矩形面积(想像成直方图)
        MaxRectSize = math.max(curMaxRectSize,MaxRectSize)
    elseif popHeight == rightBiggerHeight then
        -- 如果相等 我们可以暂且不管 
        -- 因为rightBiggerColIdx后续仍会被入栈 其与popidx能够产生完全一样的矩形 当rightBiggerColIdx被pop时会被处理 因此不会遗漏掉最大矩形的搜索
    end
end

5 最大值减最小值小于等于num的子数组数量

最大值|最小值|子数组,这些关键词意味着可以使用最大最小双端队列来计算。双端队列有两个功能:1)保证遍历过得元素中出现的极值位于队头;2)遍历过程中的不重要数据间断性抛出,使得有效性数据成有序排列(实际上栈亦有此功能,只是栈无法访问栈底)。什么是不重要数据?比如想要得知数组从某位置至当前位置的最大值,那么如果当前值比前面的值大,那么前面的值就是不重要的值,应当抛出。

local a = { 2,5,7,3,5,8 }
local num = 3
local totalCnt = 0
function getNum()
    local i,j = 1,1
    Queue qMax = Queue:create()
    Queue qMin = Queue:create()
    while i <= #a do
        while j <= #a do
            -- 构建最大最小双向队列
            while not qMax:isEmpty() and a[qMax:peek()] <= a[j] do
                -- j之前所有小的数全部弹出
                -- 构建队列的递减序列
                qMax:pop_back()
            end
            qMax:push_back(j)
            while not qMin:isEmpty() and a[qMin:peek()] >= a[j] do
                -- 构建队列的递增序列
                qMin:pop_back()
            end
            qMin:push_back(j)
            if (qMax:peek() - qMin:peek()) > num then
                -- 不满足条件 退出向后扩展过程
                -- 到达j位置才不满足条件 说明 i 至 j-1区间满足条件
                break
            end
            j = j+1 -- 后侧向后扩展
        end

        if i == qMax:peek() then
            -- i已走到最大队列的头部位置 弹出
            qMax:pop_front()
        end
        if i == qMin:peek() then
            -- 已走至最小队列头部位置 弹出
            qMin:pop_front()
        end

        totalCnt = totalCnt+((j-1)-i+1) -- [i,j-1]区间满足条件 [i,i+1] ~ [i,j-2]也一定都满足条件
        i = i+1 -- 前侧向后扩展
    end
end

6 一次数组遍历能够产生哪些栈/队形式?

1.3 树

1 基于先序与中序 还原二叉树结构

local preOrder = {1,2,4,7,3,5,6,8}
local inOrder = {4,7,2,1,5,3,8,6}

-- 递归过程
-- 每次递归 先序和中序遍历对应的子数组 均从某个位置startIdx开始 持续Len个长度
function constructTree(node,preOrderStartIdx,inOrderStartIdx,len)
    if len < 1 then
        return
    end

    -- 计算当前先序中的根节点在中序中的位置
    node.value = preOrder[preOrderStartIdx]
    local curInOrderRootIdx = getInOrderIdx(node.value,inOrderStartIdx,len)
    if not curInOrderRootIdx then
        return
    end

    -- 计算左右子节点长度
    local leftLen = curInOrderRootIdx - inOrderStartIdx
    local rightLen = len - leftLen - 1

    -- 基于先序中确定左右子节点值
    local left,right = {},{}
    node.left = left
    node.right = right

    --左右递归
    constructTree(left,preOrderStartIdx+1,curInOrderRootIdx-leftLen,leftLen)
    constructTree(right,preOrderStartIdx+leftLen+1,curInOrderRootIdx+1,rightLen)
end
function getInOrderIdx( v,inOrderStartIdx,len )
    for i=inOrderStartIdx,inOrderStartIdx+len-1 do
        if inOrder[i] == v then
            return i
        end
    end
end

-- test
local root = {}
constructTree(root,1,1,8)
function visit( node )
    if not node then
        return
    end
    print(node.value)
    visit(node.left)
    visit(node.right)
end
visit(root)

2 输入两棵二叉树A和B,判断B是否为A的子树(节点结构相同 值相同)。

function visitTree( aNode,bNode )
    if not aNode or not bNode then
        return false
    end

    if aNode.value == bNode.value then
        -- 当前节点值一致 进行子节点递归判断
        if isSubFunc(aNode,bNode) then
            return true
        end
    end

    local isSub = visitTree(aNode.left,bNode)
    -- 左子树上已找到相同子树 直接返回 无需再遍历右子树
    if isSub then 
        return true
    end
    isSub = visitTree(aNode.right,bNode)
    return isSub
end

-- 遍历完bNode 判断是否与aNode完全一致
function isSubFunc( aNode,bNode )
    -- b树当前父节点没有子节点
    -- 此处为何能直接返回true?因为此递归还有一个终止条件:aNode.value ~= bNode.value,如果能成功绕过该判断并走到B树的分支最后,说明这条分支上所有值都能满足:aNode.value == bNode.value。这是子树判断的关键。
    if not bNode then
        return true
    end
    -- b树还有子节点 但a树没有 结构不一致 非相同树
    if not aNode then
        return false
    end
    -- 节点值不同 非相同树
    if aNode.value ~= bNode.value then
        return false
    end
    -- 判断左树是否一致
    local isSub = isSubFunc(aNode.left,bNode.left)
    if not isSub then
        return false -- 若不一致 没有走右树判断的必要
    end
    isSub = isSubFunc(aNode.right,bNode.right)
    return isSub -- 返回最终判断结果
end

-- test
aNode = {value=8}
aNode.left = {value=8}
aNode.left.left = {value=9}
aNode.left.right = {value=2}
aNode.left.right.left = {value=21}
aNode.left.right.right = {value=4}
aNode.right = {value=7}
bNode = {value=8}
bNode.left = {value=9}
bNode.right = {value=2}
bNode.right.right = {value=4}
print(visitTree(aNode,bNode))

3 树的深度

计算树的深度,在树的遍历过程中加入深度统计过程即可。下面用两种方式实现。

-- 方式1:利用先序遍历计算深度
-- 根节点深度为1 方向向下 逐层加1 叶节点返回最大深度
local function preOrderForDepth(node,depth)
    if not node then
        return 0
    end

    -- depth:父节点深度 下到当前层 深度加1
    local curNodeDepth = depth + 1 
    if not node.left and not node.right then
        -- 叶子节点 返回其深度
        return curNodeDepth
    end

    local leftDepth = preOrderForDepth(node.left,curNodeDepth)
    local rightDepth = preOrderForDepth(node.right,curNodeDepth)
    return math.max(leftDepth,rightDepth)
end
-- 方式2:利用后续遍历计算速度
-- 叶节点深度为1 方向向上 逐层加1 根节点返回最大深度
local function lastOrderForDepth( node )
    if not node then
        return 0 
    end
    if not node.left and not node.right then
        -- 叶子节点 深度为1
        return 1
    end
    local leftDepth = lastOrderForDepth(node.left)
    local rightDepth = lastOrderForDepth(node.right)
    return math.max(leftDepth+1,rightDepth+1)
end
-- test
aNode = {value=8}
aNode.left = {value=8}
aNode.left.left = {value=9}
aNode.left.right = {value=2}
aNode.left.right.left = {value=21}
aNode.left.right.right = {value=4}
aNode.right = {value=7}
aNode.right.right = {value=5}
aNode.right.right.right = {value=20}
aNode.right.right.right.left = {value=11}

local rt = preOrderForDepth(aNode,0)
local rt = lastOrderForDepth(aNode)

1.3 一些实际问题

1 n个骰子的点数

 求n个骰子点数总数为某数的点数序列。一种简单粗暴的方法是采用递归的方式对每个骰子的所有点数进行累加。如何实现呢?实际上,这一对应着一个多叉树,每个骰子对应一层,每层有六个子节点(1-6),遍历每一条路径并统计其总和。这类问题很多,程序结构也是固定的:递归(完成遍历)加for循环(访问同层所有子节点)。

local target = 34
function f( n,list,curCnt )
    if n <= 0 then
        -- 递归至叶子节点 判断总和
        if curCnt == target then
            printT(list)
        end
        return
    end

    for i=1,6 do -- 处理每一个子节点
        list[n] = i
        f(n-1,list,curCnt+i) -- 深度遍历
    end
end

function printT( t )
    local s = ""
    for i,v in ipairs(t) do
        s = s .. "  " .. v
    end
    print(s)
end
-- test
f(6,{},0)

 上述问题采用遍历所有路径的方式求解所有点数序列,如果问题改一下,只需统计骰子的点数和等于某一整数值的总次数,是否还是需要全部遍历一遍树?实际上可以采用效率更高的做法。每增加一个骰子,骰子的各个点数和会出现什么变化?每个点数和次数应该是前面6个点数和出现次数的总和。

-- 方法2
function f2(n) -- 方式1
    if n < 1 then
        return 0
    end
    local a1 = {}
    local a2 = {} -- 辅助数组 存放上一次结果 
    -- 一个骰子的情形
    for i=1,6 do
        a1[i] = 1
        a2[i] = 1
    end
    -- 两个以上骰子 
    for i=2,n do
        -- 总和i之前的全部为0(每个骰子至少是1 因此总和最小为i)
        for k=1,i-1 do
            a1[k] = 0
        end
        -- 总和最大值n*6(所有骰子点数均为6)
        for k=i,n*6 do
            a1[k] = 0
            for t=k-6,k-1 do
                if a2[t] then
                    -- 每增加一个骰子 总和k出现的总数为前面6个数出现次数的总和
                    -- 如对于两个骰子 第二个骰子8出现的情形如下:第一个骰子的2+第二个骰子的6;依次类推为:3+5;4+4;5+3;6+2
                    -- 因此8出现的次数为 第一个骰子2、3、4、5、6出现次数的总和
                    a1[k] = a1[k] + a2[t]
                end 
            end
        end
        -- 更新辅助函数
        for i,v in ipairs(a1) do
            a2[i] = v
        end
    end
    -- printT(a1)
    return a1[target]
end
-- test
print(f2(6))
  • 3
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值