• 算法试题——每日一练


    删除排序链表中的重复元素

    给定一个排序链表的头结点 h e a d head head,删除所有重复的元素,使每个元素只出现一次。并返回已排序的链表。

    例如,给定一个序列[1,1,2],删除后返回[1,2]。又例如[1,1,2,2,3],删除后返回[1,2,3]。

    思想:这个算法主要就是循环的条件,我们肯定是要从前往后遍历去找到重复的结点,那么这个循环在什么情况下结束,就很重要了。其次还要考虑该链表结点的情况,可能一个都没有,可能有一个,还可能有更多。

    具体代码:

    struct ListNode
    {
    	int val;
    	struct ListNode *next;
    };
    
    struct ListNode* deleteDuplicates(struct ListNode* head)
    {
        struct ListNode *p,*q;
        //如果该链表一个结点都没有的情况
        if(!head)
            return NULL;
        //该链表只有一个结点的情况
        else if(!head->next)
            return head;
       	//该链表有两个以上结点的情况
        else
        {
            p = head;
            while(p->next)  //循环结束条件
            {
                q = p->next;  //注意这一行一定要放在循环的里面
                if(p->val == q->val)
                {
                    p->next = q->next;
                }
                else
                    p = q;
            }
        }
        return head;	//我们只是另设了两个指针用于删除重复的结点,head还是指向该链表的头结点
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    合并两个有序数组

    给定两个递增排列的数组 n u m s 1 nums1 nums1 n u m s 2 nums2 nums2 m m m表示 n u m s 1 nums1 nums1中的元素个数, n n n表示 n u m s 2 nums2 nums2中元素的个数,请将 n u m s 2 nums2 nums2中的元素合并到 n u m s 1 nums1 nums1中,且合并后 n u m s 1 nums1 nums1中的元素仍按照递增的顺序排列。

    其中, n u m s 1 nums1 nums1的数组大小为 n + m n+m n+m,后面 n n n个位置为0,应忽略。最好是设计一个算法时间复杂度 O ( m + n ) O(m+n) O(m+n)

    思想:该题的主要思想就是如何避免多次循环去排序。一般情况下,我们都是从头开始比较,对于这样的题,比较后又要将其它元素像后移以便空出位置来,这样就太麻烦了。那么为什么不从尾开始比较试一试呢?如果我们从尾开始比较,并且 n u m s 1 nums1 nums1数组后面正好空出来了 n n n个位置,比较后,将较大的元素放在 n u m s 1 nums1 nums1的最后。不过比较也要看条件,循环结束的条件就是两个数组的下标有一个小于1。循环结束后,也不要忘了继续将另一个下标不为零的数组的元素赋值到 n u m s 1 nums1 nums1中去。

    void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n)
    {
        int i = m - 1,j = n - 1,k = m + n - 1;
        while(j >= 0 && i >= 0)
        {
            if(nums2[j] >= nums1[i])
            {
                nums1[k--] = nums2[j--];
            }
            else
            {
                nums1[k--] = nums1[i--];
            }
        }
        while(j >= 0)
        {
            nums1[k--] = nums2[j--];
        }
        while(i >= 0)
        {
            nums1[k--] = nums1[i--];
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    二叉树的中序遍历

    一般面对二叉树的中序遍历,我们想到的都是递归,通过不断地调用函数实现。例如下面。

     struct TreeNode
     {
         int val;
         struct TreeNode *left;
         struct TreeNode *right;
     };
    void inorder(struct TreeNode* root,int* a,int* returnSize)
    {
        if(!root)
            return;
        else
        {
            inorder(root->left,a,returnSize);
            a[(*returnSize)++] = root->val;
            inorder(root->right,a,returnSize);
        }
    }
    
    int* inorderTraversal(struct TreeNode* root, int* returnSize)
    {
        *returnSize = 0;
        //这里分配101个地址空间是因为规定了结点个数最多100个
        int *t = (int *) malloc (sizeof(int) * 101);
        inorder(root,t,returnSize);
        return t;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    但如果我们不用递归,用迭代的方式如何做到二叉树的中序遍历呢?力扣官方给出了两种解答。

    首先,中序遍历二叉树就是要先遍历左子树、再遍历根结点、最后遍历中间结点,保持这样的顺序。那么我们就要先找到左子树最左的结点,然后它的根结点、根结点的右孩子…相当于是从最下面往上查找。那么这样也就明显了,可以利用栈先进后出的特性来做这个题(我就是没有想到这样就一直卡着,最后也是看的题解才知道)。那么我们只需要依次遍历根结点的左子树直到最左的那个结点,并在这个过程将指向每个结点的指针入栈,方便向上回溯的时候遍历它的右孩子。

     struct TreeNode
     {
         int val;
         struct TreeNode *left;
         struct TreeNode *right;
     };
    
    int* inorderTraversal(struct TreeNode* root, int* returnSize)
    {
        //栈,用来记录一条路径上指向结点的指针
        struct TreeNode **stack = (struct TreeNode **) malloc (sizeof(struct TreeNode *) * 101);
        //用来存储二叉树的中序序列
        int* a = (int *) malloc (sizeof(int) * 101);
        //作为栈的指针
        int top = 0;
        //数组的下标
        *returnSize = 0;
        //关于这里为什么是||,因为最开始top=0.假设root不为NULL
        //那么循环条件为真,继续执行,直到后面root可能为空的时候
        //这时栈里还有元素top>0,就可以继续执行了
        //可以自己带进去看一下,更好理解
        while(root || top > 0)
        {
            //沿着二叉树最左的一条路径向下,并依次将指向结点的指针入栈
            while(root)
            {
                stack[top++] = root;
                root = root->left;
            }
    		//由于上面循环结束的时候root为NULL,所以弹栈
            root = stack[--top];
            //此时root指向的结点就为最左的结点,将它并入数组中
            a[(*returnSize)++] = root->val;
            //再遍历它的右子树
            root = root->right;
        }
        return a;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    该算法的时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

    第二种算法就是 m o r r i s morris morris中序遍历。

    它能将上面算法的空间复杂度降为 O ( 1 ) O(1) O(1) m i r r i s mirris mirris遍历主要就是取消了用栈的方式来间接实现递归,用一个指针操作即可,也可以说是它利用了中序遍历的前驱和后继之间的关系。比较难懂,先介绍它的步骤( c u r cur cur为当前遍历到的结点)和代码,例子在后面

    1. c u r cur cur无左孩子,则将它的 v a l val val添加到数组中(用数组来存储二叉树的中序遍历序列),再访问 c u r cur cur的右孩子
    2. c u r cur cur有左孩子,则首先找到左子树中最右的结点(也可以理解为 c u r cur cur的前驱),并让 p r e pre pre指向它,随后判断 p r e pre pre的右孩子:
    • 若它的右孩子为空,则将其右孩子指向 c u r cur cur,然后访问 c u r cur cur的左孩子。相当于执行 p r e − > n e x t = c u r pre->next = cur pre>next=cur(前驱和后继连起来了)和 c u r = c u r − > l e f t cur = cur->left cur=cur>left
    • 若它的右孩子不为空(说明此时它的右孩子为它的后继),则此时我们已经遍历完了 c u r cur cur的左子树,将 p r e pre pre的右孩子指向NULL,然后将 c u r cur cur v a l val val添加到数组中,然后访问 c u r cur cur的右孩子。
    struct TreeNode
    {
    	int val;
    	struct TreeNode *left;
    	struct TreeNode *right;
    };
    
    int* inorderTraversal(struct TreeNode* cur, int* returnSize)
    {
    	int* t = (int *) malloc (sizeof(int) * 100);
    	struct TreeNode *pre = NULL;
    	*returnSize = 0;
    	while(cur)
    	{
    		if(cur->left)
    		{
    			pre = cur->left;
    			while(pre->right && pre->right != cur)
    			{
    				pre = pre->right;
    			}
    
    			if(!pre->right)
    			{
    				pre->right = cur;
    				cur = cur->left;
    			}
    			else
    			{
    				t[(*returnSize)++] = cur->val;
    				pre->right = NULL;
    				cur = cur->right;
    			}
    		}
    		else
    		{
    			t[(*returnSize)++] = cur->val;
    			cur = cur->right;
    		}
    	}
    	return t;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    看下图,对下图进行 m o r r i s morris morris遍历:
    在这里插入图片描述
    此时 c u r cur cur指向根结点,按照上述步骤,它有左孩子,那么使 p r e pre pre指向它左子树最右的结点,如下图:在这里插入图片描述
    此时 p r e pre pre的右孩子为空,那么按照上述步骤,将 p r e pre pre的右孩子指向 c u r cur cur,并使 c u r cur cur指向它的左孩子,如下图:在这里插入图片描述
    至此,一次循环结束。(最外层循环的条件为 c u r cur cur不为空)

    又由上图知,并按照上述步骤, c u r cur cur有左孩子,那么就使 p r e pre pre指向它左子树最右的结点,如下图:
    在这里插入图片描述
    又按照上述步骤, p r e pre pre的右孩子为空,再将 p r e pre pre的右孩子指向 c u r cur cur,并且 c u r cur cur指向它的左孩子,如下图:在这里插入图片描述
    至此,第二次循环结束。

    然后 c u r cur cur不为空,按照上述步骤, c u r cur cur无左孩子,将它的 v a l val val添加到数组中,再访问 c u r cur cur的右孩子(注意,它的右孩子不为空了,仔细看上图),如下图:在这里插入图片描述
    至此,第三次循环结束。

    然后,又按照上述步骤,此时 c u r cur cur有左孩子,又找到 c u r cur cur左子树中最右结点,并使 p r e pre pre指向它(这里跟上图没什么变化),然后继续判断, p r e pre pre的右孩子不为空,说明 c u r cur cur的左子树已遍历完,将 c u r cur cur v a l val val添加到数组中,并使 p r e pre pre的右孩子指向NULL,然后访问 c u r cur cur的右孩子,如下图:在这里插入图片描述
    至此,第四次循环结束。

    c u r cur cur不为空,又按照上述步骤, c u r cur cur无左子树,将 c u r cur cur v a l val val添加到数组中,再访问它的右孩子(注意它的右孩子为它的后继了),如下图:
    在这里插入图片描述
    至此,第五次循环结束。

    c u r cur cur不为空,它有左孩子,那么使 p r e pre pre指向它左子树中最右的结点,如下图:在这里插入图片描述
    此时 p r e pre pre的右孩子不为空,那么此时已遍历完 c u r cur cur的左子树,将 c u r cur cur v a l val val添加到数组中,并使 p r e pre pre的右孩子指向NULL,然后再访问 c u r cur cur的右孩子,如下图:在这里插入图片描述
    至此,第六次循环结束。

    c u r cur cur不为空,且它有左子树,按照上述步骤,使 p r e pre pre指向它左子树中最后的结点,如下图:在这里插入图片描述
    p r e pre pre的右孩子为空,按照上述步骤, p r e pre pre的右孩子指向 c u r cur cur c u r cur cur再访问它的左孩子,如下图:在这里插入图片描述
    至此,第七次循环结束。

    c u r cur cur不为空,但没有左子树,按照上述步骤,将它的 v a l val val添加到数组,再访问它的右孩子(注意,再之前pre已经将该结点的右孩子指向了3),如下图:在这里插入图片描述
    至此,第八次循环结束。

    c u r cur cur不为空,有左孩子,按照上述步骤,将 p r e pre pre指向它左子树中最右的结点(这里跟上图一样的),然后, p r e pre pre的右孩子不为空,按照上述步骤,则此时已经遍历完了 c u r cur cur的左子树,那么将 c u r cur cur v a l val val添加到数组中,再将 p r e pre pre的右孩子指向NULL,然后访问 c u r cur cur的右孩子,如下图:在这里插入图片描述
    至此,第9次循环结束。

    c u r cur cur为空,循环结束,返回数组的首地址。

    morris遍历,主要就是利用前驱和后继的关系来实现二叉树的中序遍历,多举例子试一下,印象就深刻了。

  • 相关阅读:
    Kernel:性能分析实例(一)
    m1 rvm install 3.0.0 Error running ‘__rvm_make -j8‘
    (CPU/GPU)粒子继承贴图颜色发射
    Android 10.0 Settings 去掉打开开发者模式和USB调试模式的广播
    人工智能-卷积神经网络之多输入多输出通道
    解释器模式
    uniapp使用uv-vi组件创建表单,收集信息
    ElasticSearch集群搭建
    代理IP与Socks5代理:跨界电商之安全防护与智能数据引擎
    NFT游戏开发 链游开发 区块链游戏开发
  • 原文地址:https://blog.csdn.net/weixin_62917800/article/details/126451316