剑指offer笔试题26~30

本文探讨了多种典型算法问题,包括复杂链表复制、二叉搜索树转换、字符串排列等,并提供了详细的解决思路与代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

26.复杂链表的复制

题目描述: 输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)
正确的思路: 首先对每个结点进行复制,并插入到原有结点的后面。第二步再逐个将新结点的特殊指针指向原有结点特殊指针所指位置的下一个位置。第三步将两个链表拆分。

/*
struct RandomListNode {
    int label;
    struct RandomListNode *next, *random;
    RandomListNode(int x) :
            label(x), next(NULL), random(NULL) {
    }
};
*/
class Solution {
public:
    RandomListNode* Clone(RandomListNode* pHead)
    {
        /*
            1.复制所有的节点,将其插在原节点的后面;
            2.将原节点的随机节点复制为新节点
            3.拆分新旧链表
        */
        if(!pHead)
            return NULL;
        
        copy(pHead);
        handle(pHead);
        return split(pHead);
    }
    void copy(RandomListNode* pHead)
    {
        RandomListNode* curNode = pHead;
        while(curNode)
        {
            RandomListNode* cloneNode = new RandomListNode(curNode->label);
            cloneNode->next = curNode->next;
            curNode->next = cloneNode;
            curNode = cloneNode->next;
        }
    }
    void handle(RandomListNode* pHead)
    {
        RandomListNode* curNode = pHead;
        while(curNode)
        {
            curNode->next->random = curNode->random == NULL?NULL:curNode->random->next;
            curNode = curNode->next->next;
        }
    }
    RandomListNode* split(RandomListNode* pHead)
    {
        RandomListNode* curNode = pHead;
        RandomListNode* cloneHead = pHead->next;
        RandomListNode* pNext = NULL;
        while(curNode)
        {
            pNext = curNode->next;
            curNode->next = pNext->next;
            pNext->next = pNext->next==NULL?NULL:pNext->next->next;
            curNode = curNode->next;
        }
        return cloneHead;
    }
};

错误的思路: 一开始我将后两步合并在一起执行,后来发现“当某个结点的特殊指针指向前面的结点时,链表前面部分已经被拆开,这样就无法找到正确的特殊指针了”。

27.二叉搜索树与双向链表

题目描述: 输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
思路:
(1) 将二叉树拆分为左子树,根节点,右子树;
(2) 先将左子树排序后,并返回一个最大的结点;
(3) 将根节点与前面返回的结点建立双向连接;
(4) 对右子树进行排序,(其中最大结点变为根结点)。
(5) 遍历双向链表找到头节点

class Solution {
public:
    TreeNode* Convert(TreeNode* pRootOfTree)
    {
        if(pRootOfTree == NULL)
            return NULL;
        //左边已经转换为双链表的最后一个结点,用于和自身连接
        TreeNode* lastNode = NULL;
        convertNode(pRootOfTree,&lastNode);
        //现在需要找到该双向链表的头节点
        TreeNode* pHead = lastNode;
        while(pHead->left)
            pHead = pHead->left;
        return pHead;
    }
    void convertNode(TreeNode* pNode,TreeNode** lastNode)
    {
        if(pNode == NULL)
            return;
        TreeNode* pCur = pNode;
        //建立与左侧结点的双向链接
        if(pCur->left != NULL)
            convertNode(pCur->left,lastNode);//从左下角开始排序
        pCur->left = *lastNode;
        if(*lastNode != NULL)
            (*lastNode)->right = pCur;
        //此时当前结点已经加入了有序链表中,因此当前结点变为最后一个结点
        *lastNode = pCur;
        //建立与右侧结点的双向连接
        if(pCur->right != NULL)
            convertNode(pCur->right,lastNode);

    }
};

28.字符串的排列

题目描述: 输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。
思路: 先假设字符串长度为 n,且不存在重复元素 ,很明显就是一个全排列的问题,结果为(Ann),这里我们就用全排列的思想,从前往后看字符串的每一位都会有几种不同情况。
(1) str[0] 可以放置所有的(n个)字符,那么将str[0]与它本身还有后面所有字符交换位置,此时首位已经确定,可用字符个数减为(n-1);
(2) str[1] 可以放置剩下的(n-1)个字符,那么将str[1]与它本身还有后面所有字符交换位置,此时次首位已经确定,可用字符个数减为(n-2);
(3) …
(4) str[n-1] 就是最后一个位置,此时可以放置的也只剩下一个字符,不用再交换了,直接跳出循环
(5) 以上的分析全是基于无重复元素,只需要再交换位置进行一下比较,如果是相同字符,那么就直接跳过这个位置,同时可用字符数减一。

class Solution {
public:
    vector<string> Permutation(string str) 
    {
        vector<string> res;
        if(str.empty())
            return res;
        //开始排列
        helper(res,str,0);
        //字典顺序输出
        sort(res.begin(), res.end());
        return res;
    }
    void helper(vector<string> &res,string str,int index)
    {
        /*
            整体思路:
            1.将首字符逐个与后面的的字符相交换,
            2.然后将次首字符与后面的字符交换
            3.直到只剩下最后一位字符,无法再继续交换
        */
        //当某字符要与其后字符交换时:发现其自身已经是最后一个字符了
        //不可能有别的组合情况了,res
        if(index == str.size()-1)
            res.push_back(str);
        //将当前子串的首字符依次与后面的字符交换位置
        for(int i=index; i<str.size(); ++i)
        {
            //首字符与其后面的字符相同,跳过
            if(i!=index && str[i]==str[index])
                continue;
            //交换位置后,获得一个新的子串[index+1:],再对该子串递归处理
            std::swap(str[i],str[index]);
            helper(res,str,index+1);
        }
    }
};

29.数组中穿线次数超过一般的数字

题目描述: 数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。
思路: 首先我们要知道这种数组的一个 特性:假如存在某一元素temp出现的次数超过长度的一半,那么我们从数组中去掉两个相异的数字,temp出现的次数还是大于剩下数组长度的一半。基于这个事实,我们每次比较两个元素:
(1) 首先选定numbers[0]为基准元素temp,此时temp出现次数count为1
(2) 如果numbers[1]与temp相同,计数count加一
(3) 如果numbers[1]与temp不同,计数count减一
(4) 在此过程中肯定会遇到 计数为零的情况, 计数为零说明前面的元素为成对相异,因此我们直接选定剩下子数组的首元素为新的基准temp,这并不会破坏该数组的特性。

class Solution {
public:
    /*
        首先我们要知道这种数组的哟个特性:
        假如存在某一元素temp出现的次数超过长度的一半,
        那么我们从数组中去掉两个相异的数字,temp出现的次数还是大于剩下数组长度的一半。
        基于这个事实,我们每次比较两个元素:
        1.首先选定numbers[0]为基准元素temp,此时temp出现次数count为1
        2.如果numbers[1]与temp相同,计数count加一
        3.如果numbers[1]与temp不同,计数count减一
        在此过程中肯定会遇到计数为零的情况,计数为零说明前面的元素均为成对相异,
        因此我们直接选定剩下子数组的首元素为新的额基准temp,这并不会破坏该数组的特性。
    */
    int MoreThanHalfNum_Solution(vector<int> numbers) 
    {
        if(numbers.empty())
            return 0;
        //初始状态选定首元素为基准元素,并设定计数为1
        int count = 1;
        int temp = numbers[0];
        for(int i=1; i<numbers.size(); ++i)
        {
            //前面出现了成对的互异元素,直接选定新的基准元素
            if(count == 0)
            {
                temp = numbers[i];
                count = 1;
            }
            else if(numbers[i] == temp)
                count++;
            else
                count--;
        }
        if(checkMoreThanHalf(numbers,temp))
            return temp;
        else
            return 0;
    }
    //判断temp出现的次数是否超过总数的一半
    bool checkMoreThanHalf(vector<int>&numbers,int temp)
    {
        int count = 0;
        for(auto &x:numbers)
            if(x == temp)
                count++;
        if(count > numbers.size()>>1)
            return true;
        return false;
    }
};

30.最小的k个数

题目描述: 输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。
思路1: 这是一个topk的问题,首先想到堆排序,先对前k个数据进行构造大根堆,然后将剩下的元素逐个与根节点比较,小于根节点时将其与根节点交换并下滤。时间复杂度为nlogk。

class Solution {
public:
    vector<int> GetLeastNumbers_Solution(vector<int> input, int k) 
    {
		vector<int> result;
		if(input.empty() || input.size()<k)
			return result;
		//构建前k个元素的大根堆
		for(int i=k/2-1; i>=0; --i)
		{
			percDown(input,i,k);
		}
		//剩余的元素逐个与根节点进行比较
		//如果大于等于根节点,跳过
		//如果小于根节点,替换根节点进行下滤
		for(int i=k; i<input.size(); ++i)
		{
			if (input[0] > input[i])
			{
				std::swap(input[0],input[i]);
				percDown(input,0,k);
			}
		}
		//比较结束后,再对k个元素的大根堆进行删除操作
		//删除并不用真正的删除操作,只需要将最大的根节点和最后一个元素交换位置,
		//再令数组大小减一,并对新的根节点进行下滤
		for(int j=k-1; j>0 ; --j)
		{
			std::swap(input[0],input[j]);
			percDown(input,0,j);
		}
        for(int i=0; i<k; ++i)
            result.push_back(input[i]);
		return result;
    }
private:
	void percDown(vector<int> &arr,int i,int n)
	{
		int temp;
		int child;
		for(temp=std::move(arr[i]); getchildChild(i)<n; i=child)
		{
			child = getchildChild(i);
			//判断是否有两个儿子,有就选出最大的那个根自己比较
			if (child!=n-1 && arr[child]<arr[child+1])
				child += 1;
			if (temp < arr[child])
				arr[i] = arr[child];//因为儿子大,所以将儿子的位置提升,此时儿子原来的位置就空出来了
			else
				break;
			//arr[i]下滤可能会导致其大儿子的子树失去大根的特性,因此要令i=child,对其儿子再进行下滤
		}
		arr[i] = temp;//将temp填在空出来的位置
	}
	int getchildChild(int i)
	{
		return 2*i+1;
	}
};

思路二: 直接对整个数组进行插入排序,然后返回前k和数据。

class Solution {
public:
    vector<int> GetLeastNumbers_Solution(vector<int> input, int k) 
    {
        //当vector为空,或者k>n直接返回一个空的vector
        if(input.size() == 0 || k > input.size())
        {
            vector<int> result;
            return result;
        }
            
        //使用插入排序对整个vector排序
        for(int cur=1;cur<input.size();++cur)
        {
            int tmp = std::move(input[cur]);
            int j;
            for(j = cur; j>0 && tmp<input[j-1]; --j)
                input[j] = input[j-1];
            input[j] = std::move(tmp);
        }
        //构造一个vector,包含input前k个元素
        vector<int> result(input.begin(),input.begin()+k);
        return result;
    }
};

思路三: 采用红黑树mutiset。

class Solution {
public:
    typedef multiset<int,greater<int>> inSet;//降序的set
    typedef multiset<int,greater<int>>::iterator setIterator;
    vector<int> GetLeastNumbers_Solution(vector<int> input, int k) 
    {
        //当vector为空,或者k>n直接返回一个空的vector
        vector<int> result;
        if(input.size() == 0 || k > input.size())
        {
            vector<int> result;
            return result;
        }
        inSet leastNumbers;
        auto iter = input.begin();
        for( ; iter!=input.end(); ++iter)
        {
            if (leastNumbers.size()<k)
                leastNumbers.insert(*iter);
            else
            {
                setIterator iterGreatest = leastNumbers.begin();
                if(*iter < *iterGreatest)
                {
                    leastNumbers.erase(iterGreatest);
                    leastNumbers.insert(*iter);
                }
            }
        }
        //此时的leastNumbers为:4,3,2,1为什么可以运行成功呢`
        for(auto &i:leastNumbers)
            result.push_back(i);
        return result;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值