剑指Offer

总算看完《剑指Offer》这本书,发现里面说的很多东西确实很有道理,记一些比较经典的题目。包括之前的旋转数组位运算都出自这本书。

总结了以下规律:
1.写函数的时候要考虑:常规输入、边界输入、违法输入。
2.有些细节能注意就注意。比如尽量用指针传递复杂类型的参数,因为值传递,从形参到实参会产生一次复制操作。再如C#中不要多次用String的+运行来拼接字符串,因为这样会产生很多临时String,最好用StringBuilder的Append。
2.数组问题基本上都是O(n)或者O(logN)问题,一般数组都要想到从两头(或者从尾部)同时开始操作,数组操作尽量考虑用指针,排序的数组考虑二分,否则充分考虑归并、快排算法中的性质。
3.链表问题基本上都是O(n)问题,就是看能不能想到方法了,一般都是2~3个指针,充分利用链表的性质,需要倒序的时候想到栈。
4.二叉树问题基本上都是递归问题,而且都得考虑它的3种遍历方式,要注意什么遍历的方式是在递归的哪不步进行操作的,考虑用栈或者队列。
5.位运算是解题的关键:&、|、^、<<、>>,数的二进制操作分别做这些操作之后会怎样。
6.画图、举例、分解、对比f(n)和f(n-1)看是否有规律可循、模拟运行的过程、空间换时间。
7.递归要领:不要去想这个递归是的过程一步步循环是怎样的,要想这个函数确实就可以得到想要的结果,然后考虑边界和函数结束条件。
最后,最实在的规律,就是多看书。。多做题。。

1.斐波那契数列
f(n) = f(n-1) + f(n-2),f(0)=0,f(1)=1.
用循环做时间复杂度为O(N),有O(logN)的算法,暂不做介绍。
这道题有很多变形,比如一只青蛙一次可以跳1级,也可以跳2级。求跳上一个n级的台阶共有多少种跳法。

2.数值的整数次方
实现函数double Power(double base, int exponent),求base的exponent次方,不考虑大数问题。

//double判断是否相等 不能直等
bool Equal(double x, double y)
{
    if (x - y < eps && x - y > -eps)
        return true;
    return false;
}

//更加高效的求pow方法 
// n为偶数 a^n = a^(n/2) * a^(n/2)
// n为奇数 a^n = a^((n-1)/2) * a^((n-1)/2) * a
double PowerWithUnsignedExponet(double base, unsigned int x)
{
    if (x == 0)
        return 1;
    if (x == 1)
        return base;

    double result = PowerWithUnsignedExponet(base, x >> 1);
    result *= result;

    //由于奇数的二进制表示中最后一位必为1,可以进行位与运算比较最后一位
    if (x & 0x1 == 1)
        result *= base;

    return result;
}


//求base的x次方
//考虑base是0和x是负数的情况
double Power(double base, int x)
{
    if (Equal(base, 0.0))
        return 0.0;

    int abs = x;
    if (x < 0)
        abs = -abs;

    double result = PowerWithUnsignedExponet(base, (unsigned int)abs);
    //double result = 1.0f;
    //for (int i = 1; i <= abs; i++)
    //  result *= base;
    //
    if (x < 0)
        result = 1.0 / result;

    return result;
}

3.在O(1)时间删除链表结点
常规做法是从头遍历链表,直到要删除结点的前一个结点,然后进行删除操作。
O(1)方法是把下一个结点的内容复制到要删除的结点上,把要删除结点的next指向下一结点的next,然后删除下一结点。当然如果是尾结点的话,就需要遍历到它前面的那个节点然后再进行删除了。

所以首先,判断链表是否为空以及要删除节点是否为空;
然后,判断删除的结点不是尾结点(next!=NULL);
然后,判断链表是否只有一个结点(即删除头结点);
最后,如果有多个结点且删除的是尾结点。

但是该方法假设要删除的结点的确在链表中。

//链表结点定义
struct ListNode
{
    int m_nValue;
    ListNode* m_pNext;
};

//O(1)时间删除结点
void DeleteNode(ListNode* pListHead, ListNode* pToBeDeleted)
{
    if (pListHead == NULL || pToBeDeleted == NULL)
        return;

    //不是尾结点
    if (pToBeDeleted->m_pNext)
    {
        ListNode* deleteP = pToBeDeleted->m_pNext;
        pToBeDeleted->m_nValue = deleteP->m_nValue;
        pToBeDeleted->m_pNext = deleteP->m_pNext;
        delete deleteP;
        deleteP = NULL;
    }
    //只有一个结点,删除头结点
    else if (pToBeDeleted == pListHead)
    {
        delete pToBeDeleted;
        pToBeDeleted = NULL;
        pListHead = NULL;
    }
    //删除的是尾结点
    else if (pToBeDeleted->m_pNext == NULL)
    {
        ListNode* prev = pListHead;
        while (prev->m_pNext != pToBeDeleted)
            prev = prev->m_pNext;

        prev->m_pNext = NULL;
        delete pToBeDeleted;
        pToBeDeleted = NULL;
    }
}

4.调整数组顺序使奇数位于偶数前面
二分,一个头指针,一个尾指针,头指针一直到偶数停下,尾指针一直到奇数停下,然后交换头尾指针的数据,反复直到两指针相遇。

//把一个数组的奇数调整位于偶数前面
void ReOrderMatrix(int A[], int length)
{
    if (A == NULL || length == 0)
        return;

    int front = -1;
    int rear = length;

    while (front < rear)
    {
        //向后移动直到找到第一个偶数
        while ( front < rear && (A[++front] & 0x1) != 0);

        //向前移动直到找到第一个奇数
        while ( front < rear && ( A[--rear] & 0x1) == 0);

        if(front < rear)
            swap(A[front], A[rear]);
    }
}

当然,必然有高级的做法。。

//比较高级的 传了一个函数作为参数。。所以函数可以是别的,比如整除3之类的
void ReOrderMatrix(int A[], int length, bool(*func)(int))
{
    if (A == NULL || length == 0)
        return;

    int front = -1;
    int rear = length;

    while (front < rear)
    {
        //向后移动直到找到第一个偶数
        while (front < rear && !func(A[++front]) );

        //向前移动直到找到第一个奇数
        while (front < rear && func(A[--rear]));

        if (front < rear)
            swap(A[front], A[rear]);
    }
}

bool isEven(int n)
{
    return (n & 1) == 0;
}

int main()
{
    int A[10] = { 1,2,3,4,5,6,7,8,9,10 };
    ReOrderMatrix(A, 10, isEven);
    for (int i = 0; i < 10; i++)
        cout << A[i] << " ";
}

5.链表中倒数第k个结点
输入一个链表,求倒数第k个结点。从1开始计数,如链表 {1,2,3,4,5,6},倒数第一个就是6,倒数第三个就是4。

两个指针,一个先走k-1步,然后两个同时走,当第一个做到末尾时,第二个刚好就指向倒数第k个,因为他与末尾指针相差了k-1个。

唔..注意链表判空,k<0,链表长度

//链表结点定义
struct ListNode
{
    int m_nValue;
    ListNode* m_pNext;
};

ListNode* FindKthToTail(ListNode* pListHead, unsigned int k)
{
    if (pListHead == NULL || k <= 0)
        return NULL;

    ListNode* first = pListHead;
    ListNode* second = pListHead;

    for (int i = 0; i < k - 1; i++)
    {
        if (first->m_pNext)
        {
            first = first->m_pNext;
        }
        else
            return NULL;
    }

    while (first->m_pNext)
    {
        first = first->m_pNext;
        second = second->m_pNext;
    }

    return second;
}

6.反转链表
定义一个函数,输入链表的头结点,反转该链表并输出反转后链表的头结点。

//反转链表并返回反转后的头结点
ListNode* ReverseList(ListNode* pListHead)
{
    if (pListHead == NULL)
        return NULL;

    ListNode* prev = NULL;
    ListNode* curr = pListHead;
    ListNode* next = NULL;
    ListNode* newHead = NULL;
    while (curr->m_pNext)
    {
        next = curr->m_pNext;
        if (next->m_pNext == NULL)
            newHead = next;

        curr->m_pNext = prev;
        prev = curr;
        curr = next;
    }
    return newHead;
}

7.合并两个排序的链表

这里写图片描述

首先一定记得判空,然后每次选两个链表头结点中值较少的插入新链表。

//合并两个排序的链表
ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
{
    if (pHead1 == NULL)
        return pHead2;
    else if (pHead2 == NULL)
        return pHead1;

    ListNode* newHead = NULL;

    if (pHead1->m_nValue < pHead2->m_nValue)
    {
        newHead = pHead1;
        newHead->m_pNext = Merge(pHead1->m_pNext, pHead2);
    }
    else
    {
        newHead = pHead2;
        newHead->m_pNext = Merge(pHead1, pHead2->m_pNext);
    }
    return newHead;
}

8.树的子结构
输入两课二叉树A和B,判断B是不是A的子结构。

这里写图片描述

首先在A中查找与B的根结点一样的值,然后用递归的方法继续查找其余结点。
代码很简洁,只能说递归很强。。

struct BinaryTreeNode
{
    int m_nValue;
    BinaryTreeNode* m_pLeft;
    BinaryTreeNode* m_pRight;
};

//判断一棵二叉树是否是另一棵二叉树的子结构
bool DoesTree1HasTree2(BinaryTreeNode* A, BinaryTreeNode* B)
{
    if (B == NULL)
        return true;

    if (A == NULL)
        return false;

    if (A->m_nValue != B->m_nValue)
        return false;

    return DoesTree1HasTree2(A->m_pLeft, B->m_pLeft) && DoesTree1HasTree2(A->m_pRight, B->m_pRight);
}

bool HasSubtree(BinaryTreeNode* A, BinaryTreeNode* B)
{
    bool result = false;

    if (A != NULL && B != NULL)
    {
        if (A->m_nValue == B->m_nValue)
            result = DoesTree1HasTree2(A, B);

        if (!result)
            result = HasSubtree(A->m_pLeft, B);

        if (!result)
            result = HasSubtree(A->m_pRight, B);
    }

    return result;
}

9.顺时针打印矩阵
按照从外向里以顺时针的顺序一次打印出每一个数字。
这里写图片描述

这里写图片描述
相当于每次打印矩阵中的一个圈。
假设行数是rows,列数是columns。打印第一圈的左上角坐标是(0,0),第二圈是(1,1)。所以,左上角行列标号总相同。所以可以用(start,start)来表示一圈的开始。

然后,每一次打印一圈都会使矩阵减少2行和2列(最后一次打印除外),那么循环结束的条件就是columns>startX*2 && rows>startY*2。因为第一圈从(0,0)开始,所以最后一圈是刚好满足这个条件,可以举例试试:如5x5,最后一圈是(2,2),6x6,最后一圈也是(2,2)。

除最后一圈外,每一圈都有包括四步:由左到右、由上到下、由右到左、由下到上。

最后一圈有以下3种情况:
这里写图片描述

那么第一步总是需要;第二步条件是终止行大于起始行;第三步条件是终止行大于起始行,且终止列大于起始列;第四步条件是终止行大于起始行至少2,且终止列大于起始列。

//顺时针打印数组
void PrintMatrixInCircle(int** numbers, int columns, int rows, int start)
{
    int endX = columns - 1 - start;
    int endY = rows - 1 - start;

    //第一步 由左至右
    for (int i = start; i <= endX; i++)
        cout << numbers[start][i] << " ";

    //第二步 由上至下
    if (endY > start)
    {
        for (int i = start+1; i <= endY; i++)
            cout << numbers[i][endX] << " ";
    }

    //第三步 由右至左
    if (endY > start && endX > start)
    {
        for (int i = endX-1; i >= start; i--)
            cout << numbers[endY][i] << " ";
    }

    //第四步 由下至上
    if (endY > start+1 && endX > start)
    {
        for (int i = endY-1; i >= start+1; i--)
            cout << numbers[i][start] << " ";
    }

}

void PrintMatrixClockwisely(int** numbers, int columns, int rows)
{
    if (numbers == NULL || columns <= 0 || rows <= 0)
        return;

    int start = 0;
    while (columns > (2*start) && rows > (2*start) )
    {
        PrintMatrixInCircle(numbers, columns, rows, start);
        start++;
    }
}

int main()
{
    int A[4][4] = { {1,2,3,4},{5,6,7,8},{9,10,11,12},{13,14,15,16} };
    //定义了二维指针数组 指针是定义在栈空间上的
    int** ppl = new int*[4];
    //这里直接把指针指向了二维数组 所以最后指针不需要delete 因为没有申请空间
    for (int i = 0; i < 4; i++)
        ppl[i] = A[i];

    PrintMatrixClockwisely(ppl, 4, 4);
    cout << endl;
    return 0;
}

10.栈的压入、弹出序列
给定两个整数序列,第一个表示栈的压入顺序,请判断第二个是否为该栈的弹出顺序。假设入栈的所有数字均不相等,如1,2,3,4,5是压栈序列,4,5,3,2,1是对应的一个弹出序列,而4,3,5,1,2就不是。

利用一个辅助栈,把输入的第一个序列依次压入,按照第二个序列的顺序依次弹出。
如果下一个弹出的数字刚好是栈顶,则直接弹出。如果不在栈顶,那么把压栈序列中没有入栈的数字入栈,直到下一个需要弹出的数字在栈顶。如果所有数字都入栈仍然没找到弹出的数字,那么该序列就不是弹出的一个序列。

//栈的压入、弹出序列
bool IsPopOrder(const int* pPush, const int* pPop, int length)
{
    bool ok = false;
    if (pPush && pPop && length > 0)
    {
        //const 在*前面 说明(*p)不能改变 但是能改变p的指向
        const int* pNextPush = pPush;
        const int* pNextPop = pPop;

        stack<int> stackData;

        while (pNextPop - pPop < length)
        {
            while (stackData.empty() || stackData.top() != *pNextPop)
            {
                //所有数字已入栈
                if (pNextPush - pPush == length)
                    break;

                stackData.push(*pNextPush);
                pNextPush++;
            }
            if (stackData.top() != *pNextPop)
                break;

            stackData.pop();
            pNextPop++;
        }

        if (stackData.empty() && pNextPop - pPop == length)
            ok = true;
    }
    return ok;
}

11.二叉搜索树的后序遍历序列
输入一个数组,判断该数组是不是某二叉搜索树的后序遍历的结果。注意是二叉搜索树,并且假设没有重复。

后序遍历中,前半部分是左子树,后半部分是右子树,最后才是根。而前半部分应该小于根,后半部分应该大于根。然后递归判断左、右子树是否满足条件。

//判断一个后序遍历是否是一棵二叉搜索树
bool VertifySequenceOfBST(int sequence[], int length)
{
    if (sequence == NULL || length <= 0)
        return false;

    int root = sequence[length - 1];

    //在二叉搜索树中左子树的结点应该小于根
    int i = 0;
    for (; i < length - 1; i++)
        if (sequence[i] > root)
            break;

    //在二叉搜索树中右子树应该大于根
    int j = i;
    for (; j < length - 1; j++)
        if (sequence[j] < root)
            return false;

    //判断左子树是不是二叉搜索树
    bool left = true;
    if (i > 0)
        left = VertifySequenceOfBST(sequence, i);

    bool right = true;
    if (i < length - 1)
        right = VertifySequenceOfBST(sequence + i, length - i - 1);

    return (left && right);
}

12.二叉树中和为某一值的路径
输入一棵二叉树和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。从根结点开始往下直接叶结点所经过的结点形成一条路径。

首先,一定是一条到叶子结点的路径,所以要到叶子结点在能判断路径的和。
其次,由于路径从上往下,所以用先序遍历,每次需要记录当前路径上的结点,而且到达一个叶子结点后需要返回到其父结点继续遍历。因此需要用一个栈来保存路径,而由于最后我们需要输出所有的路径,所以可以vector来代替stack。

//输出二叉树中和为某以值的路径
void FindPath(BinaryTreeNode* pRoot, int expectedSum)
{
    if (pRoot == NULL)
        return;

    vector<int> path;
    int currentSum = 0;
    FindPath(pRoot, expectedSum, path, currentSum);
}

void FindPath(BinaryTreeNode* pRoot, int expectedSum, vector<int> &path, int &currentSum)
{
    currentSum += pRoot->m_nValue;
    path.push_back(pRoot->m_nValue);

    //如果是叶子结点,并且路径上的和等于输入的值,输出路径
    bool isLeaf = (pRoot->m_pLeft == NULL && pRoot->m_pRight == NULL);
    if (expectedSum == currentSum && isLeaf)
    {
        cout << "Path: ";
        vector<int>::iterator it = path.begin();
        for (; it != path.end(); it++)
            cout << *it << " ";
        cout << endl;
    }

    //如果不是叶子结点 遍历它的子结点
    if (pRoot->m_pLeft != NULL)
        FindPath(pRoot->m_pLeft, expectedSum, path, currentSum);
    if (pRoot->m_pRight != NULL)
        FindPath(pRoot->m_pRight, expectedSum, path, currentSum);

    //在返回父结点之前,在路径上删除当前结点并减去当前结点的值
    currentSum -= pRoot->m_nValue;
    path.pop_back();
}

13.复杂链表的复制
实现一个函数,复制一个复杂链表。所谓复杂链表意思是,每个链表结点除了有一个next指针之外,还有一个sibling指针,该指针指向链表中的任意结点或者NULL。

这里写图片描述

O(n)时间做法1:
首先常规复制链表,只指定next指针,并且过程中定义一个map把新旧结点信息存储下来。
然后第二遍遍历的时候复制biling指针就可以通过map直接找到。
该做法需要O(n)的空间。
O(n)时间做法2:
在复制链表的时候,不是另外新建一个链表,而是把复制的结点插入到原来链表的对应结点后面。
这里写图片描述
这样第二步的时候设置每个结点的sibling指针就很方便了,因为都是next的关系,不需要遍历查找,O(1)就能找到。
第三步就是把这个链表拆分成2个链表,奇数上的结点是原链表,偶数上的结点是复制的链表。

14.二叉搜索树与双向链表
输入一棵二叉搜索树,讲该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
这里写图片描述

首先,二叉搜索树的中序遍历就是从小到大顺序遍历二叉搜索树的每一个结点。当遍历到根结点时,它应该跟左子树最大一个结点以及右子树最小一个结点链接起来。
这里写图片描述

把左、右子树都转换成排序的双向链表之后再和根链接起来,整个二叉搜索树就变成了排序的双向链表。

按照中序遍历的顺序,当转换到根结点(10)时,它的左子树已经转换成一个排序的链表了,并且处在链表的最后一个结点是当前值最大的结点。只要把这个结点(8)和根结点链接起来,此时链表最后一个结点就是根结点了(10)。然后再遍历转换右子树,并把跟结点和右子树中最小的结点链接起来。
至于转换它的左右子树,跟遍历的过程一样,用递归。

//转换二叉搜素树为双向链表
void ConvertNode(BinaryTreeNode* pNode, BinaryTreeNode** pLastNodeInList)
{
    if (pNode == NULL)
        return;

    BinaryTreeNode* pCurrent = pNode;

    //跟中序遍历一样 先递归当前结点左子树
    if (pCurrent->m_pLeft)
        ConvertNode(pCurrent->m_pLeft, pLastNodeInList);

    //然后处理当前结点
    pCurrent->m_pLeft = *pLastNodeInList;
    if (*pLastNodeInList)
        (*pLastNodeInList)->m_pRight = pCurrent;

    *pLastNodeInList = pCurrent;

    //然后递归当前结点右子树
    if (pCurrent->m_pRight)
        ConvertNode(pCurrent->m_pRight, pLastNodeInList);
}

BinaryTreeNode* Convert(BinaryTreeNode* pRoot)
{
    //用pLastNodeInList指向已经转换好的链表的最后一个结点
    BinaryTreeNode* pLastNodeInList = NULL;
    //传指针引用 要改变指针p的值,没有&只是传一个指针
    //不加&而且函数会以这个指针的形参进行操作,不会改变p的值
    ConvertNode(pRoot, &pLastNodeInList);

    BinaryTreeNode* pHeadOfList = pLastNodeInList;
    while (pHeadOfList && pHeadOfList->m_pLeft)
        pHeadOfList = pHeadOfList->m_pLeft;

    return pHeadOfList;
}

15.字符串的排列
输入一个字符串,打印出该字符串中字符的所有排列。例如输入abc,则打印出由字符a、b、c所能排列出来的所有字符串abc、acb、bac、bca、cab、cba。

求整个字符串的排列,可以看成两步:
首先求所有可能出现在第一个位置的字符串,即把第一个字符和后面所有的字符交换。
然后求后面字符的排列,就又可以把后面字符看成第一个字符和后面字符的排列。

关于这道题的拓展:
1.输入一个含有8个数字的数组,判断有么有可能把这8个数字分别放到正方体的8个顶点上,使得正方体上三组相对的面上的4个顶点的和相等。

思路:相当于求出8个数字的全排列,判断有没有一个排列符合题目给定的条件,即三组对面上顶点的和相等。

2.N皇后问题:在8 X 8的国际象棋上摆放八个皇后,使其不能相互攻击,即任意两个皇后不得处于同一行,同一列或者同意对角线上,求出所有符合条件的摆法。

思路:由于8个皇后不能处在同一行,那么肯定每个皇后占据一行,这样可以定义一个数组A[8],数组中第i个数字,即A[i]表示位于第i行的皇后的列号。先把数组A[8]分别用0-7初始化,接下来对该数组做全排列,由于我们用0-7这7个不同的数字初始化数组,因此任意两个皇后肯定也不同列,那么我们只需要判断每个排列对应的8个皇后中是否有任意两个在同一对角线上即可,即对于数组的两个下标i和j,如果i-j==A[i]-A[j]或i-j==A[j]-A[i],则认为有两个元素位于了同一个对角线上,则该排列不符合条件。
参考链接:http://blog.csdn.net/libing13820393394/article/details/45072029?ref=myread

//求字符串的排列
void Pernutation(char* pStr, char* pBegin)
{
    if (*pBegin == '\0')
        cout << pStr << endl;
    else
    {
        for (char* pCh = pBegin; *pCh != '\0'; ++pCh)
        {
            char temp = *pCh;
            *pCh = *pBegin;
            *pBegin = temp;

            Pernutation(pStr, pBegin + 1);

            temp = *pCh;
            *pCh = *pBegin;
            *pBegin = temp;         
        }
    }
}

void Pernutation(char* pStr)
{
    if (pStr == NULL)
        return;

    Pernutation(pStr, pStr);
}

16.数组中出现次数超过一半的数字
如一个长度为9的数组{1,2,3,2,2,2,5,4,2},其中2出现了5次,超过数组长度一半,输出2。

如果数组是排序的,O(n)情况下就能找出。但是题目数组不是排序的,而数组排序是O(nlogn),所以不能直接先排序。

解法1:基于Partition函数的O(n)算法
数组中某个数字出现了超过数组长度的一半,如果这个数组是排序的,那么数组的中位数(data[n/2])就是这个数字。
这种方法利用快排的Partition,先在数组中随机选一个数字,然后调整数组中数字的顺序,使得比选中数字小的在它左边,大的在它右边。如果这个选中的数字最后刚好是n/2,那么它就是中位数。否则,它大于n/2则中位数在它左边,小于n/2则中位数在它右边。然后只要递归在它对应的部分查找。
最后,要注意判断输入是否是空数组;以及最后找到的那个数是否真的超过一半长度(再做一次遍历)。

解法2:根据数组特点O(n)算法
如果一个数字出现次数超过一半,也就是它出现的次数要比其他所有数字出现的次数和要多。
考虑在遍历数组的时候保存两个值:一个是数组中的一个数字,一个是次数。当遍历到下一个数字时,如果下一个数字和之前保存的数字相同,则次数加1;如果下一个数字和之前的数字不同,次数减1。如果次数为0,我们需要保存下一个数字,并把次数设为1。由于要找的数字的次数比其他所有数字出现次数之和多,那么要找的数字肯定是最后一次把次数设为1时对应的数字。(就是一命换一命,最后命多的队伍留下来)

//找出数组中出现次数超过一半的数字
int MoreThanHalfNum(int* numbers, int length)
{
    if (numbers == NULL || length <= 0)
        return 0;

    int result = numbers[0];
    int times = 1;
    for (int i = 1; i < length; ++i)
    {
        if (times == 0)
        {
            result = numbers[i];
            times = 1;
        }
        else if (numbers[i] == result)
            times++;
        else
            times--;
    }

    //判断是否真的出现了一半次数以上
    times = 0;
    for (int i = 0; i < length; i++)
    {
        if (numbers[i] == result)
            times++;
    }

    if (times * 2 <= length)
        result = 0;
    return result;
}

17.最小的K个数
输入n个整数,找出其中最小的k个数。如输入4、5、1、6、2、7、3、8这8个数字,最小的4个数字是1、2、3、4。

解法一:基于Partition函数的O(n)算法
跟上题一样,只是把n/2改成了k而已。

解法二:O(nlogk)的算法
维护一个大小为k的最大堆,每次有新的数据进来的时候,如果该数字比堆顶大,那么堆顶出堆,该数字入堆。(优先队列,插入和删除都是O(logk),总的效率就是O(nlogk))

可以priority_queue做,不过书上说的是用multiset做,multiset和set都是采用红黑树实现的。红黑树通过把结点分为红、黑两种颜色并根据一些规则来确保树在一定程度上是平衡的,从而保证红黑树中查找、删除和插入操作都是O(logk)。
关于红黑树:http://blog.csdn.net/chenhuajie123/article/details/11951777
关于multiset:http://blog.csdn.net/hnust_xiehonghao/article/details/7942541

该算法适合海量数据的输入,因为它只需要保存k个数而且不会修改原数组。

//找出数组中最小的k个数
//set和multiset里面的元素都是从小到大(或从大到小)排序的
//并且保证插入、删除、查找等操作都在O(logn)完成
//greater<int> 在 头文件 <functional> 中
void GetLeastNumbers(int* numbers,int length, int k)
{
    multiset<int, greater<int>> leastNumbers;

    if (k < 1 || numbers == NULL || length < k)
        return;

    for (int i = 0; i < length; i++)
    {
        if (leastNumbers.size() < k)
            leastNumbers.insert(numbers[i]);
        else
        {
            multiset<int, greater<int>>::iterator it = leastNumbers.begin();
            if (numbers[i] < *(leastNumbers.begin()))
            {
                leastNumbers.erase(it);
                leastNumbers.insert(numbers[i]);
            }
        }
    }

    multiset<int, greater<int>>::iterator it = leastNumbers.end();
    do
    {
        it--;
        cout << *it << " ";
        if (it == leastNumbers.begin())
            break;
    } while (1);
    cout << endl;
}

18.连续子数组的最大和
输入一个整型数组,里面有正也有负。数组中的一个或者连续的多个整数组成一个子数组,求所有子数组的和的最大值,要求O(n)。

解法一:实实在在的分析
两个变量,一个sum,一个Max。遍历的过程中,如果sum<0,则设sum为当前数字,否则,sum=sum+当前值。然后Max=max(sum,max)。

//找出数组中连续子数组的最大和
int FindGreatestSumOfSubArray(int* numbers, int length)
{
    if (numbers == NULL || length <= 0)
        return 0;

    int sum = 0;
    int Max = 0x80000000;  //负数最大值
    for (int i = 0; i < length; ++i)
    {
        if (sum <= 0)
            sum = numbers[i];
        else
            sum += numbers[i];

        if (sum > Max)
            Max = sum;
    }
    return Max;
}

解法二:动态规划
这个的话不详细说,因为说实话动态规划我自己都不是很清楚,贴个图:

这里写图片描述

19.从1到n整数中1出现的次数
输入一个整数n,求从1到n这n个整数的十进制表示中1出现的次数。例如输入12,从1到12这些整数中包含1的数字有1,10,11和12,一共出现了5次。

这里书上的代码是对的,但是解释有一点点错。
首先是21345做例子,分两段:1~1345和1346~21345。
1346~21345中,首先看首位,1出现在10000~19999中,共1000次。但是如果首位=1,出现次数就是后面的数字+1了(如12345,出现次数=2346)。然后是1出现在除最高位之外的四位数字中的情况,看代码注释。
然后就是求1~1345中1出现的次数,这个次数可以用递归求。

//求1到n个整数中1出现的次数
int NumberOf1(const char* strN)
{
    if(strN == NULL || *strN <'0' || *strN > '9' || *strN == '\0')
        return 0;

    int first = *strN - '0';
    unsigned int length = static_cast<unsigned int>(strlen(strN));

    if (length == 1 && first == 0)
        return 0;

    if (length == 1 && first > 0)
        return 1;

    //假设strN是 21345
    //numFirstDigit是数字 10000~19999 的第一个位中的数目
    //这个可以理解 >1 那么以1打头的就会出现 10^(n-1)次方
    // =1 那么出现次数=后面的数字+1
    int numFirstDigit = 0;
    if (first > 1)
        numFirstDigit = pow(10, length - 1);
    else if (first == 1)
        numFirstDigit = atoi(strN + 1) + 1;

    //numOtherDigits 是 1346~21345 除了第一位之外的数位中的数目
    //这里很关键 书上解释错了 
    //首先分2段没错 1346~11345 11346~21345 
    //然后是每段都要求1在后4位中出现的次数
    //每段其实都相差10000个 设想是从0~9999,那么1可以在4个位置任意出现,而其他位置都可以是0~9的任意数字
    //比如1346~21345: 1346~9999的可以理解, 0~1345的就是 10000~11345 所以才有上面的说法
    //所以每段是 4*10^3 一共两段就是 2*4*10^3=8000(书上说2000其实是错的) 所以有下面的代码
    int numOtherDigits = first * (length - 1) * pow(10, length - 2);

    //numRecursive 是 1~1345 中的数目
    int numRecursive = NumberOf1(strN + 1);

    return numFirstDigit + numOtherDigits + numRecursive;
}

//将数字转到字符串
int NumberOf1Between1AndN(int n)
{
    if (n <= 0)
        return 0;

    char strN[50];
    sprintf(strN, "%d", n);

    return NumberOf1(strN);
}

搜了一下,有其他解法而且比较容易理解的。参考链接:
http://blog.csdn.net/yi_afly/article/details/52012593

这里解释的比较清楚,从个位、十位打上进行分析的。

public int count(int n){
    if(n<1)
        return 0;
    int count = 0;
    int base = 1;
    int round = n;
    while(round>0){
        int weight = round%10;
        round/=10;
        count += round*base;
        if(weight==1)
            count+=(n%base)+1;
        else if(weight>1)
            count+=base;
        base*=10;
    }
    return count;
}

20.把数组排成最小的数
输入一个正整数数组,把数组所有数字拼成一个数,打印出最小的一个。如{3,32,321},打印321323。

找出一个排序规则,数组根据这个规则排序之后能排成一个最小的数字。
两个数字m和n能拼成mn和nm。如果mn

//把数组排成最小的数
int compare(const void* strNumber1, const void* strNumber2)
{
    char g_StrCombine1[21], g_StrCombine2[21];

    //这里确实不是很理解 但是如果要自己写的话我应该会写一个结构体用Sort
    strcpy(g_StrCombine1, *(const char**)strNumber1);
    strcat(g_StrCombine1, *(const char**)strNumber2);

    strcpy(g_StrCombine2, *(const char**)strNumber2);
    strcat(g_StrCombine2, *(const char**)strNumber1);

    return strcmp(g_StrCombine1, g_StrCombine2);
}

void PrintMinNumber(int* numbers, int length)
{
    if (numbers == NULL || length <= 0)
        return;

    char** strNumbers = (char**)(new int[length]);
    for (int i = 0; i < length; ++i)
    {
        strNumbers[i] = new char[11];
        sprintf(strNumbers[i], "%d", numbers[i]);
    }

    //这里用了qsort 其实可以自己一个结构体 然后写compare 用C++ 里面的sort
    qsort(strNumbers, length, sizeof(char*), compare);

    for (int i = 0; i < length; i++)
        cout << strNumbers[i];
    printf("\n");

    for (int i = 0; i < length; ++i)
        delete[] strNumbers[i];
    delete[] strNumbers;
}

21.丑数
把只含因子2、3、5的数称作丑数。求从小到大的顺序的第1500个丑数。例如6、8都是丑数,但14不是,它包含因子7。把1当做第一个丑数。

暴力做法,每一个数都判断是不是丑数(一直除2直到不能再除,然后是3,然后是5,最后等于1就是丑数),直到找到1500个。

上面做法会针对每个数进行计算,即使不是丑数也会进行判断。
那么其实可以把计算好的丑数放在一个数组中,而下一个丑数,就是数组中的每个丑数x2,x3,x5,然后取最小的那个。
那肯定也不用遍历整个数组让整个数组都乘,它一定有一个边界值,使得x2(x3,x5)刚好大于当前数组的最大值。所以只要记住这个值,然后计算。
这种做法增加了空间消耗,需要一个数组存放丑数。1500个就是6KB。

//计算第n个丑数
int Min(int a, int b, int c)
{
    int min = a < b ? a : b;
    min = min < c ? min : c;
    return min;
}

int GetUglyNumber(int index)
{
    if (index <= 0)
        return 0;

    int *pUglyNumbers = new int[index];
    pUglyNumbers[0] = 1;
    int nextUglyIndex = 1;

    //记录每个因子的临界值
    int *pMultiply2 = pUglyNumbers;
    int *pMultiply3 = pUglyNumbers;
    int *pMultiply5 = pUglyNumbers;

    while (nextUglyIndex < index)
    {
        int min = Min(*pMultiply2 * 2, *pMultiply3 * 3, *pMultiply5 * 5);
        pUglyNumbers[nextUglyIndex++] = min;

        while (*pMultiply2 * 2 <= min)
            pMultiply2++;

        while (*pMultiply3 * 3 <= min)
            pMultiply3++;

        while (*pMultiply5 * 5 <= min)
            pMultiply5++;
    }
    int ugly = pUglyNumbers[index - 1];
    delete[] pUglyNumbers;
    return ugly;
}

22.第一个只出现一次的字符
在字符串中找出第一个只出现一次的字符。如输入”abaccdeff”,输出b。

一个哈希数组来记录次数,这样两次O(n)就能找到该字符。
注意字符不一定是字母,有可能是其他符号。1和char类型是1个字节大小,所以有256种可能,数组大小就是256,用ASCII码做键值。
同样需要一个辅助空间。

//第一个只出现一次的字符
char FirstNotRepeatingChar(char* pString)
{
    if (pString == NULL)
        return '\0';

    const int tableSize = 256;
    unsigned int hashTable[tableSize];
    for (unsigned int i = 0; i < tableSize; ++i)
        hashTable[i] = 0;

    char* pHashKey = pString;
    while (*pHashKey != '\0')
    {
        hashTable[*(pHashKey++)]++;
    }

    pHashKey = pString;
    while ( *pHashKey != '\0' )
    {
        if (hashTable[*pHashKey] == 1)
            return *pHashKey;
        pHashKey++;
    }
    return '\0';
}

扩展:
1.如果要在第一个字符串中删除在第二个字符串中出现的所有字符。那么就用一个hash表来保存第二个字符出现的次数,然后遍历第一个字符串,O(1)找到次数,就可以做相应操作,时间复杂度O(n)。
2.删除重复字符,同样的一个hash表保存次数。另外加一个bool表来记录是否是第一次出现,不是则删除,时间复杂度同样是O(n)。
3.如果字符不止256个,比如改成汉字,就有很多个。
那么想到的一个方法就是用一个unsigned int 的1位来表示一个字符,1表示只出现1次,0表示出现0次或者1次以上。

想到TX一道笔试题目,用32个unsigned int整型来记录1024个什么东西的状态,那么一个int整型是4个字节,无符号就是32都可以用,每位都可以用1和0来表示一个的状态。

void IsJob(unsigned int* job, unsigned int a, unsigned b)
{
    if (a < 1 || a > 1024 || b < 1 || b > 1024)
    {
        cout << "-1\n";
        return;
    }

    //先减1 让数字在 0~31
    a = a - 1;
    b = b - 1;

    job[a / 32] = job[a / 32] | (0x01 << (a % 32));

    unsigned int test = 0x01 << (b % 32);
    if (job[b / 32] & test)
    {
        cout << "1\n";
    }
    cout << "0\n";
}

23.数组中的逆序对
一个数组中的两个数字如果后面的数字小于前面的数字,则它们构成一个逆序对。求数组中逆序对的总数。

如果每个数字都跟后面的比较,那么是O(n^2)。

这里写图片描述

这里写图片描述

首先把数组一直平分,一直到最后只剩下一个数字,然后进行合并。
一个数字合并的时候,如果前面的数字比后面的数字大,那么逆序对数加1(否则不加),然后把它存到另一个辅助数组,接下来把后面的数字存到辅助数组。所以辅助数组是一个排序后的数组,而且统计了合并的时候产生的逆序对。每个数字都做这样的合并,所以得到的都是排好序的子数组,然后递归对这些子数组进行合并。
合并的规则像上图,这个过程类似与归并排序,时间复杂度是O(nlogn)。

//求数组中的逆序对
int InversePairsCore(int* data, int* copy, int start, int end)
{
    if (start == end)
    {
        copy[start] = data[start];
        return 0;
    }

    int length = (end - start) / 2;
    int left = InversePairsCore(copy, data, start, start + length);
    int right = InversePairsCore(copy, data, start + length + 1, end);

    // i初始化为前半段最后一个数字的下标
    int i = start + length;
    // j初始化为后半段最后一个数字的下标
    int j = end;
    int indexCopy = end;
    int count = 0;
    while (i >= start && j >= start + length + 1)
    {
        if (data[i] > data[j])
        {
            copy[indexCopy--] = data[i--];
            count += j - start - length;
        }
        else
        {
            copy[indexCopy--] = data[j--];
        }
    }

    for (; i >= start; --i)
        copy[indexCopy--] = data[i];

    for (; j >= start + length + 1; --j)
        copy[indexCopy--] = data[j];

    return left + right + count;
}

int InversePairs(int* data, int length)
{
    if (data == NULL || length < 0)
        return 0;

    int* copy = new int[length];
    for (int i = 0; i < length; ++i)
        copy[i] = data[i];

    int count = InversePairsCore(data, copy, 0, length - 1);
    delete[] copy;

    return count;
}

24.两个单向链表的第一个公共结点
假设两个链表长度分别是m和n。
由于是单链表,如果这两个链表有同一个结点,那么从这个结点开始,两个链表后面的结点都是重合的(因为只有一个next)。

这里写图片描述

方法一:空间O(m+n),时间O(m+n)
从尾到头遍历,直到最后一个相同的结点。由于是单链表,所以需要两个辅助栈。

方法二:时间O(m+n),不需要额外空间
首先得到两个链表的长度,然后让长的链表的头指针先走它们之间的差,接下来同时走,走到第一个相同的结点就是第一个公共结点。(像上图中,只要第一个链表的指针先走1步,然后再在同一个循环中一起走)。

25.数字在排序数组中出现的次数
统计一个数字在排序数组中出现的次数。例如输入{1,2,3,3,3,3,4,5}和3,输出4。

首先想到的,二分查找3,然后在该位置左右进行遍历,时间复杂度O(n)。

如何用二分查找在数组中找到第一个k和最后一个k。
二分查找中,先跟k比较,如果比k大,那么继续查找前半段;如果比k小,继续查找后半段;当等于k的时候,我们先判断它前面的数字是不是k,如果不是,那么它就是第一个k;如果是,那么第一个k在前半段,那么继续在前半段查找。对于最后一个k,就是判断后面的是不是也是k。

//找到排序数组中第一个重复的k
int GetFirstK(int* numbers, int k, int start, int end)
{
    if (start > end)
        return -1;

    int mid = (end + start) / 2;
    int midValue = numbers[mid];

    if (midValue > k)
        end = mid - 1;
    else if (midValue < k)
        start = mid + 1;
    else
    {
        if ( (mid > 0 && numbers[mid - 1] != k) || mid == 0 )
            return mid;
        else
            end = mid - 1;
    }
    return GetFirstK(numbers, k, start, end);
}

//找到排序数组中最后一个重复的k
int GetLastK(int* numbers, int k, int start, int end)
{
    if (start > end)
        return -1;

    int mid = (end + start) / 2;
    int midValue = numbers[mid];

    if (midValue > k)
        end = mid - 1;
    else if (midValue < k)
        start = mid + 1;
    else
    {
        if( (mid < end && numbers[mid + 1] != k) || mid == end)
            return mid; 
        else
            start = mid + 1;
    }
    return GetLastK(numbers, k, start, end);
}

int GetNumberOfK(int* numbers, int length, int k)
{
    int number = 0;

    if (numbers && length > 0)
    {
        int first = GetFirstK(numbers, k, 0, length - 1);
        int last = GetLastK(numbers, k, 0, length - 1);

        if (first > -1 && last > -1)
            number = last - first + 1;
    }

    return number;
}

26.二叉树的深度
输入一个二叉树的根结点,求该树的深度。从根结点到叶子结点一次经过的结点(含根、叶子结点)形成树的一条路径,最长路径的长度为树的深度。

递归,如果一棵树只有一个结点,深度为1。
如果有左子树而没有右子树,深度为左子树深度加1。
右子树同理,如果都有,取深度较大的值加1。

//二叉树的深度
int TreeDepth(BinaryTreeNode* pRoot)
{
    if (pRoot == NULL)
        return 0;

    int left = TreeDepth(pRoot->m_pLeft);
    int right = TreeDepth(pRoot->m_pRight);

    return left > right ? (left + 1) : (right + 1);
}

判断该二叉树是不是平衡二叉树,如果任意左右子树的结点深度差不超过1,那么它就是一棵平衡二叉树。

//判断二叉树是否平衡
bool IsBalanced(BinaryTreeNode* pRoot)
{
    if (pRoot == NULL)
        return true;

    int left = TreeDepth(pRoot->m_pLeft);
    int right = TreeDepth(pRoot->m_pRight);
    if (left - right > 1 || left - right < -1)
        return false;

    return IsBalanced(pRoot->m_pLeft) && IsBalanced(pRoot->m_pRight);
}

递归的代码有重复遍历的情况,最好就是每个结点只遍历一次。
如果用后序遍历的方式,每次遍历到一个结点之前我们就已经遍历了它的左右子树,只要在遍历的时候记录它的深度,就可以一边遍历一边判断每个结点是不是平衡的。

//判断二叉树是否平衡 递归只需一次遍历
//调用 int depth =0;  IsBalanced(pRoot, &depth);
bool IsBalanced(BinaryTreeNode* pRoot, int* pDepth)
{
    if (pRoot == NULL)
    {
        *pDepth = 0;
        return true;
    }

    int left, right;
    if (IsBalanced(pRoot->m_pLeft, &left) && IsBalanced(pRoot->m_pRight, &right))
    {
        if (left - right >= -1 || left - right <= 1)
        {
            *pDepth = 1 + (left > right ? left : right);
            return true;
        }
    }
    return false;
}

27.数组中只出现了一次的数字
一个整型数组除了两个数字之外,其他数字都出现了两次,找出这两个数字。如{2,4,3,6,3,2,5,5},4和6都只出现了一次。要求时间复杂度O(n),空间复杂度O(1)。

首先,一个数异或(^)它自己等于0。那么如果一个数组中只有一个只出现了一次的数字,那么用0依次异或数组的每一个数字,最后的结果就是那个只出现一次的数字。(比如:1^2^3^2^3=1,只有它们出现了两次,都会相互抵消,与顺序无关)。
然后就是问题分解,试着把原始数组分成两个子数组,使得每个子数组包含一个只出现一次的数字,而其他数字都成对出现。这样只要在每个子数组找到那个只出现一次的数字就行。
用0从头到尾遍历,最后剩下的数字肯定是两个不一样的数字的异或结果。找到这个结果中从右边起第一个为1的位的位置(因为异或,所以该位必然一个是1,另外一个是0),记为n。然后用该位来分两个数组,该位为1的一个,该位为0的一个。

//数组中只出现一次的数字
//在整数num的二进制表示中找到最右边是1的位
unsigned int FindFirstBitIs1(int num)
{
    int indexBit = 0;
    //当最右边的位是0 并且 index < 32 ( int字节数x8位)
    while ((num & 1) == 0 && (indexBit < 8 * sizeof(int)))
    { 
        num = num >> 1;
        ++indexBit;
    }
    return indexBit;
}

//判断num在二进制表示中从右边起的indexBit位是不是1
bool IsBit1(int num, unsigned int indexBit)
{
    num = num >> indexBit;
    return (num & 1);
}

void FindNumApperOnce(int* data, int length)
{
    if (data == NULL || length < 2)
        return;

    int resultExclusiveOR = 0;
    for (int i = 0; i < length; i++)
        resultExclusiveOR ^= data[i];

    unsigned int indexOf1 = FindFirstBitIs1(resultExclusiveOR);
    int num1=0, num2 = 0;
    for (int i = 0; i < length; i++)
    {
        if (IsBit1(data[i], indexOf1))
            num1 ^= data[i];
        else
            num2 ^= data[i];
    }
    cout << num1 << " " << num2 << endl;
}

28.和为s的两个数字VS和为s的连续正数序列
输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字,输出任意一对即可。
{1,2,3,7,11,15}和15,输出4和11。

由于数组是排好序的,所以一个头指针,一个尾指针。如果这两个数和比s大,那么尾指针前移,否则头指针后移。直到找到刚好相等的两个数。时间复杂度O(n)

输入一个整数s,打印出所有和为s的连续正数序列(至少含有2个数)。
输入15,由于1+2+3+4+5=4+5+6=7+8=15,所以结果打印出3个连续序列1~5,4~6,7~8。

还是两个指针,这次两个指针连续,一个small,一个big,每次big移动,大于s则small移动,小于则big继续移动,等于则输出。

//和为s的连续整数序列
void FindContinuousSequence(int sum)
{
    if (sum < 3)
        return;

    int small = 1;
    int big = 2;
    //small到了mid 就终止了
    int mid = (1 + sum) / 2;
    int curSum = small + big;

    while (small < mid)
    {
        if (curSum == sum)
        {
            for (int i = small; i <= big; i++)
                cout << i << " ";
            cout << endl;
        }

        while (curSum > sum && small < mid)
        {
            curSum -= small;
            small++;

            if (curSum == sum)
            {
                for (int i = small; i <= big; i++)
                    cout << i << " ";
                cout << endl;
            }
        }
        big++;
        curSum += big;
    }
}

29.翻转单词顺序VS左旋转字符串
输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。
如”I am a student.”,输出”student. a am I”。

这道题是我,我直接一个入栈出栈。
书中的思路是先翻转整个句子,然后翻转每个单词。

//字符串中一段的翻转
void Reverse(char* pBegin, char* pEnd)
{
    if (pBegin == NULL || pEnd == NULL)
        return;

    while (pBegin < pEnd)
    {
        char temp = *pBegin;
        *pBegin = *pEnd;
        *pEnd = temp;

        pBegin++, pEnd--;
    }
}

char* ReverseSentence(char* pData)
{
    if (pData == NULL)
        return NULL;

    char* pBegin = pData;
    char* pEnd = pData;
    while (*pEnd != '\0')
        pEnd++;
    pEnd--;

    //先整个句子的翻转
    Reverse(pBegin, pEnd);

    pEnd = pBegin;
    while (1)
    {
        if (*pEnd == ' ' || *pEnd == '\0')
        {
            Reverse(pBegin, --pEnd);
            if (*pEnd == '\0')
                break;
            else        //指向下一个单词的开头
            {
                pEnd += 2;
                pBegin = pEnd;
            }
        }
        else
            pEnd++;
    }
    return pData;
}

字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。定义一个函数实现该功能,比如输出数字”abcdefg”和2,输出”cdefgab”。

知识的迁移。。
把”abcdefg”看成”ab”和”cdefg”看成两部分,然后分别翻转得到”bagfedc”,然后再整体翻转就得到”cdefgab”了。

//字符串的左移
char* LeftRotateString(char* pStr, int n)
{
    if (pStr != NULL)
    {
        int length = static_cast<int>(strlen(pStr));
        if (n < length && n>0 && length > 0)
        {
            char* pBegin = pStr;
            char* pEnd = pStr + n -1;
            Reverse(pBegin, pEnd);
            pBegin = pEnd+1;
            pEnd = pStr + length - 1;
            Reverse(pBegin, pEnd);
            pBegin = pStr;
            Reverse(pBegin, pEnd);
        }
    }
    return pStr;
}

30.n个骰子的点数
把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。

那个骰子最小值为n,最大值为6n。所有点数的排列为6^n,要统计出每一个点数出现的次数,然后除以6^n,就是概率。

解法一:基于递归,有重复计算
把骰子分成1和n-1,计算从1到6的每一种和剩下的n-1个骰子来计算点数和,然后递归。

解法二:基于循环求骰子点数,时间性能好
考虑用两个数组来存储骰子点数和每一个总数出现的次数。在一次循环中,第一个数组的第n个数字表示骰子和为n出现的次数。在下一次循环中,加了一个新的骰子,此时和为n的骰子出现的次数应该等于上一次循环中骰子点数和为n-1、n-2、n-3、n-4,n-5与n-6的次数的总和。

//n个骰子点数和出现的概率
void PrintProbability(int number)
{
    if (number < 1)
        return;

    int* pProbabilities[2];
    pProbabilities[0] = new int[6 * number + 1];
    pProbabilities[1] = new int[6 * number + 1];
    for (int i = 0; i < 6 * number + 1; ++i)
    {
        pProbabilities[0][i] = 0;
        pProbabilities[1][i] = 0;
    }

    //一个骰子时候 1~n 出现次数都是1
    int flag = 0;
    for (int i = 1; i <= 6; ++i)
        pProbabilities[flag][i] = 1;

    for (int k = 2; k <= number; ++k)
    {
        for (int i = 0; i < k; ++i)
            pProbabilities[1 - flag][i] = 0;

        for (int i = k; i <= 6 * k; ++i)
        {
            //一个数组的第n项等于另一数组的n-1、n-2、n-3、n-4、n-5以及n-6的和
            pProbabilities[1 - flag][i] = 0;
            for (int j = 1; j <= i && j <= 6; ++j)
            {
                pProbabilities[1 - flag][i] += pProbabilities[flag][i - j];
            }
        }
        //下一轮循环中 交换数组
        flag = 1 - flag;
    }
    double total = pow(6.0, number);
    for (int i = number; i <= 6 * number; ++i)
    {
        double ratio = (double)pProbabilities[flag][i] / total;
        printf("%d: %lf\n", i, ratio);
    }
    delete[] pProbabilities[0];
    delete[] pProbabilities[1];
}

31.扑克牌的顺子
从扑克牌中随机抽5张,判断是不是一个顺子,即这5张牌是不是连续的。A为1,J为11,Q为12,K为13,大、小王可以是任意数字。

首先把5张牌看成一个数组,然后进行排序。大、小王暂且先用0代替。
然后排序后统计0的个数,最后统计排序后数组的相邻数字直接的空缺总数。如果总数小于或则等于0,那么这个数组就是连续的。
最后要注意,如果出现非0重复数字,就是不连续的。

//判断5张扑克牌是否为顺子
bool IsContinuous(int* numbers, int length)
{
    if (numbers == NULL || length < 1)
        return false;

    sort(numbers, numbers + length);

    int numberOfZero = 0;
    int numberOfGap = 0;

    //统计0的个数
    for (int i = 0; i < length && numbers[i] == 0; ++i, ++numberOfZero);

    //统计数组中的间隔数目
    int small = numberOfZero;
    int big = small + 1;
    while (big < length)
    {
        //两个数相等 有对子
        if (numbers[small] == numbers[big])
            return false;

        numberOfGap += numbers[big] - numbers[small] - 1;
        small = big;
        ++big;
    }
    return numberOfGap > numberOfZero ? false : true;
}

32.圆圈中最后剩下的数字(约瑟夫环)
0,1,2,…n-1这n个数字构成一个圆圈,从0开始每次在这个圆圈里面删除第m个数字。求最后剩下的数字。

这里写图片描述

从0开始每次删除第3个数字,则删除的顺序是2、0、4、1,最后剩下的是3。

解法一:用环形链表模拟圆圈
用一个链表来模拟整个过程。
时间复杂度O(mn),空间复杂度O(n)。

//约瑟夫环 用std::list 链表来模拟
int LastRemaining(unsigned int n, unsigned int m)
{
    if (n < 1 || m < 1)
        return -1;

    unsigned int i = 0;

    list<int> numbers;
    for (int i = 0; i < n; ++i)
        numbers.push_back(i);

    list<int>::iterator current = numbers.begin();
    while (numbers.size() > 1)
    {
        for (int i = 1; i < m; ++i)
        {
            current++;
            if (current == numbers.end())
                current = numbers.begin();
        }

        list<int>::iterator next = ++current;
        if (next == numbers.end())
            next = numbers.begin();

        --current;
        numbers.erase(current);
        current = next;
    }
    return *current;
}

解法二:找规律
一堆分析。。不写了,有兴趣了再去百度分析,最后得到递归公式。

这里写图片描述

//约瑟夫环 用递推公式
int LastRemainingFormula(unsigned int n, unsigned int m)
{
    if (n < 1 || m < 1)
        return -1;

    int last = 0;
    for (int i = 2; i <= n; ++i)
        last = (last + m) % i;
    return last;
}

33.求1+2+..+n
求1+2+..n,不能用乘除法、for、while、if、else、switch、case等关键字和判断语句(A?B:C)。

额。。这个不想多讲了,好像并没有什么意义,把前面的都搞清楚了才是王道。

这里写图片描述

33.不用加减乘除做加法
求两个整数之和,不能使用+、-、乘、除。

不能用四则运算只剩下位运算了。
第一步各位相加但不计算进位(异或操作)。
第二步记下进位。(与操作+左移)
第三步进位和不计算进位的值相加(重复第一步和第二步),直到不再产生进位。

//位运算实现加法
int AddWithBitOperation(int num1, int num2)
{
    int sum, carry;
    do
    {
        sum = num1 ^ num2;
        carry = (num1 & num2) << 1;
        num1 = sum;
        num2 = carry;
    } while (num2 != 0);

    return num1;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值