网游服务器端设计思考:心跳设计

网络游戏服务器的主要作用是模拟整个游戏世界,客户端用过网络连接把一些信息数据发给服务器,在操作合法的情况下,更新服务器上该客户端对应的player实体、所在场景等,并把这些操作及其影响广播出去。让别的客户端能显示这些操作。

    在这个模拟过程中,需要解决的一个重要问题是:多长时间处理(更新)一次该服务器上的待处理事件,体现在实际开发中,这就是一个服务器端的心跳设计问题(tick)。

    在网络游戏服务器端的心跳设计里主要面临以下几个问题:

  1. 心跳函数的处理间隔往往是固定的,因为需要模拟现实世界中时间的性质,不能让游戏世界表现得忽快忽慢。但处理间隔固定不代表一定要和真实时间一致,有可能有快放慢放的需求。
  2. 固定间隔的心跳,间隔多少长?50ms,100ms,500ms?
  3. 由于服务器每次心跳处理的事件数量和复杂度不一样,每次处理所需的时间也会不同,服务器繁忙时和闲置时相差很远,应该使用什么策略来应对?
  4. 编码实现时应该怎么设计?是和游戏主循环在同一个线程里,还是把心跳写到一个单独的timer线程里,或者干脆做成一台心跳服务器(心跳指令定期通过TCP发出,或者通过同步卡),逻辑服务器都由心跳服务器控制tick的频率。
  5. 心跳必须和逻辑程序写在一个进程空间里吗?有没有以独立运行的心跳服务?

    为了解决以上问题,本文将对心跳进行分类,从不同角度进行讨论。

 

一、按照策略分类

    就心跳间隔策略而言,现在的网游服务器端主要分为两种。分别是固定tick时间和固定sleep时间,可以通过下图进行具体的说明:

image 

图 1-1

    如上图1-1中,画出两种间隔策略的示意图,渐变颜色的横条代表时间,Tick1、Tick2代表程序两次不同的更新操作,Run1、Run2代表在心跳函数里处理更新操作所需的时间,Sleep1、Sleep2代表让出CPU时间片的时间。

  (1)固定Tick时间:顾名思义就是指程序每次心跳的时间都是等长的、固定的。如图中的“图A”,Tick1和Tick2的时间是相等的,如果实际执行的比上次执行时间长(Run2 > Run1),则Sleep2 < Sleep1,同时满足等式:Tick1 = Tick2 = Run1 + Sleep1 = Run2 + Sleep2

  (2)固定Sleep时间:每次心跳,更新操作执行完成后,sleep固定的时间。如图中的“图B”,Sleep1 = Sleep2,Run1和Run2不一定相等,同时满足等式:Tick1 = Run1 + Sleep1,Tick2 = Run2 + Sleep2

    下面结合具体的代码对比说明这两种策略

 

1.1 固定Tick时间

    使用固定tick时间的心跳策略的一大好处就是,在负荷不高的情况下,由于相邻两次tick的时间一定,所以开始执行Run1到开始执行Run2的时间间隔一定。tick时间固定带来的另一个好处就是容易实现逻辑服务器运行时快放慢放功能(见??),当然固定tick时间同样带来一些问题,如下图:

image

图 1-2

    如图1-2,在负荷不高的情况下,心跳函数可以按照上图中“图A”的时间线正常的运行,如果在服务器运行的过程中遇到一些突发事件(开新服、做活动、大世界内大范围的帮战),会导致服务器CPU负荷变高,从而使得一次tick无法处理完当前所有事件,出现“图B”中的情况Run1 > Tick1,这时Sleep1不管取什么值都不能满足等式Tick1 = Run1 + Sleep1,

    这样一来就带来第一个问题:高负荷情况下如何保证CPU能充分利用的情况下,tick1和tick2两次心跳互相不干扰?伴随而来的另一个问题是tick时间设为多长才能满足低负荷时固定间隔的要求,同时不能经常出现“图B”的情况?

    下面结合实例讲解固定tick时间的心跳如何编写,以及如何处理以上两个问题。

 

Mangos-Zero

    mangos-zero项目中的逻辑服务进程mangosd的心跳函数采用如图1-2中的“图C”的方法,当更新的处理时间Run1大于固定大小的tick时间时,下一个tick到来时不sleep直接执行Run2,实现代码如下:

 

   1: /// Heartbeat for the World
   2: void WorldRunnable::run()
   3: {
   4:     ///- Init new SQL thread for the world database
   5:     WorldDatabase.ThreadStart();     // let thread do safe mySQL requests (one connection call enough)
   6:     sWorld.InitResultQueue();
   7:  
   8:     uint32 realCurrTime = 0;
   9:     uint32 realPrevTime = WorldTimer::tick();
  10:  
  11:     uint32 prevSleepTime = 0;        // used for balanced full tick time length near WORLD_SLEEP_CONST
  12:  
  13:     ///- While we have not World::m_stopEvent, update the world
  14:     while (!World::IsStopped())
  15:     {
  16:         ++World::m_worldLoopCounter;
  17:         realCurrTime = WorldTimer::getMSTime();  //----------------(1)
  18:  
  19:         uint32 diff = WorldTimer::tick();        //--------------(2)
  20:  
  21:         sWorld.Update( diff );                   //--------------(3)
  22:         realPrevTime = realCurrTime;
  23:  
  24:         // diff (D0) include time of previous sleep (d0) + tick time (t0)
  25:         // we want that next d1 + t1 == WORLD_SLEEP_CONST
  26:         // we can't know next t1 and then can use (t0 + d1) == WORLD_SLEEP_CONST requirement
  27:         // d1 = WORLD_SLEEP_CONST - t0 = WORLD_SLEEP_CONST - (D0 - d0) = WORLD_SLEEP_CONST + d0 - D0
  28:         if (diff <= WORLD_SLEEP_CONST+prevSleepTime)    //----------------(4)
  29:         {
  30:             prevSleepTime = WORLD_SLEEP_CONST+prevSleepTime-diff;
  31:             ACE_Based::Thread::Sleep(prevSleepTime);
  32:         }
  33:         else
  34:             prevSleepTime = 0;
  35:  
  36:         #ifdef WIN32
  37:             if (m_ServiceStatus == 0) World::StopNow(SHUTDOWN_EXIT_CODE);
  38:             while (m_ServiceStatus == 2) Sleep(1000);
  39:         #endif
  40:     }
  41:  
  42:     sWorld.KickAll();                // save and kick all players
  43:     sWorld.UpdateSessions( 1 );      // real players unload required UpdateSessions call
  44:  
  45:     // unload battleground templates before different singletons destroyed
  46:     sBattleGroundMgr.DeleteAllBattleGrounds();
  47:  
  48:     sWorldSocketMgr->StopNetwork();
  49:  
  50:     sMapMgr.UnloadAll();             // unload all grids (including locked in memory)
  51:  
  52:     ///- End the database thread
  53:     WorldDatabase.ThreadEnd();       // free mySQL thread resources
  54: }

 

    以上代码是游戏世界的主循环,看while循环里的代码,主要干下面几件事:

(1)从WorldTimer::getMSTime()得到一个uint32的值realCurrTime,realCurrTime是循环的(到增加到0xFFFFFFFF后,在增加就变成0),表示当前时间,单位是毫秒,是一个相对前一次tick的时间。

(2)使用WorldTimer::tick();计算上次tick到这次tick的时间差diff,该值理论上等于realCurrTime – realPrevTime

(3)sWorld.Update( diff );就是tick里的处理函数,游戏逻辑在这里得到更新处理。

(4)这里就是图1-2中的“图C”所描述的,如果运行时间大于固定的tick时间,则不sleep继续占用CPU来处理更新,直到能在一个tick处理所有操作为止,这个时候才会sleep让出CPU时间片。

(5)WORLD_SLEEP_CONST就是固定的tick的时间长度,在这里是50ms

    总结:现在可以回答本节前面的两个问题:在高负荷情况下mangos采用图1-2中“图C”的方式提高服务器的响应速度,每个tick时间长度为50ms,也就是每秒钟更新20次,能满足更新的需求。

 

timer_thread

    出于模块化的考虑,固定tick时间策略还有一种实现方式:使用单独的线程做timer,周期性产生心跳信号或者心跳task。工作原理如下图:

 

image 

图 1-3

    图1-3也是比较常见的设计方案,服务器进程采用多线程方式,主循环线程、timer线程及其他非工作线程向任务队列(task queue)中添加task,而工作线程不断的从任务队列中取出任务执行相应的处理。这里提到的timer thread就是用来产生心跳任务的,timer thread会每隔50ms产生一个heartbeat task放入任务队列中。一般来说队列中的heartbeat task的数量会远远大于其他task,所以这种策略也可以称为固定tick时间的心跳策略。在服务器高负荷运行的情况下,近似于mangos所采用的图1-2中“图C”的方式进行处理。多个worker thread的情况下,还需要对heartbeat task加锁。

 

1.2 固定Sleep时间

    固定Sleep也是一种比较常见的心跳函数间隔处理策略,如下图,每次心跳处理函数执行完毕后sleep固定长度时间。

 

image

    图 1-4

    如图1-4,Sleep1 = Sleep2,Run1和Run2不一定相等,同时满足等式:Tick1 = Run1 + Sleep1,Tick2 = Run2 + Sleep2。下面结合实例进行说明:

 

天龙

    根据网上流出的天龙源代码,GameServer工程的主循环至线程的心跳函数的调用过程如下:

 

   1: BOOL Server::Loop( )
   2: {
   3:     ........
   4:  
   5:     ret = g_pThreadManager->Start( ) ; //--------(1)
   6:  
   7:     ........
   8:  
   9:      return TRUE ;
  10: }
  11:                     |
  12:                     |
  13:                    \|/
  14: BOOL ThreadManager::Start( )
  15: {
  16:     ........
  17:  
  18:     BOOL ret ;
  19:     m_pServerThread->start() ;               //--------(2)
  20:     MySleep( 500 ) ;
  21:     ret = m_pThreadPool->Start( ) ;
  22:     
  23:     ........
  24: }
  25:                     |
  26:                     |
  27:                    \|/
  28: VOID Thread::start () 
  29: { 
  30:     ........
  31:  
  32: #if defined(__LINUX__)
  33:     pthread_create( &m_TID, NULL , MyThreadProcess , this );
  34: #elif defined(__WINDOWS__)
  35:     m_hThread = ::CreateThread( NULL, 0, MyThreadProcess , this, 0, &m_TID ) ;
  36: #endif
  37:  
  38:     ........
  39: }
  40:                     |
  41:                     |
  42:                    \|/
  43: VOID ServerThread::run( )
  44: {
  45:     ........
  46:  
  47:     _MY_TRY
  48:     {
  49:         g_pServerManager->m_ThreadID = getTID() ;
  50:  
  51:         while( IsActive() )
  52:         {
  53:             if( g_pServerManager )
  54:             {
  55:                 BOOL ret = g_pServerManager->Tick( ) ;   //--------(3)
  56:                 Assert( ret ) ;
  57:             }
  58:  
  59:             ........
  60:         }
  61:     }
  62:     _MY_CATCH
  63:     {
  64:         ........
  65:     }
  66:  
  67:     ........
  68: }

 

    如上,主循环启动一个“用来处理服务器之间数据通讯的线程”m_pServerThread,以及一个线程池m_pThreadPool。首先看m_pServerThread的run ()函数,调用g_pServerManager->tick ()函数,代码如下:

 

   1: BOOL ServerManager::Tick( )
   2: {
   3:     ........
   4:  
   5:     BOOL ret ;
   6:  
   7:     _MY_TRY
   8:     {
   9:         ret = Select( ) ;       //--------(1)
  10:         Assert( ret ) ;
  11:     }
  12:     _MY_CATCH
  13:     {
  14:         SaveCodeLog( ) ;
  15:     }
  16:  
  17:     ........
  18: }
  19:                     |
  20:                     |
  21:                    \|/
  22: BOOL ServerManager::Select( )
  23: {
  24: __ENTER_FUNCTION
  25:  
  26:     MySleep(50) ;             //--------(2)
  27:     if( m_MaxFD==INVALID_SOCKET && m_MinFD==INVALID_SOCKET )
  28:     {
  29:         return TRUE ;
  30:     }
  31:  
  32:     m_Timeout[SELECT_USE].tv_sec  = m_Timeout[SELECT_BAK].tv_sec;
  33:     m_Timeout[SELECT_USE].tv_usec = m_Timeout[SELECT_BAK].tv_usec;
  34:  
  35:     m_ReadFDs[SELECT_USE]   = m_ReadFDs[SELECT_BAK];
  36:     m_WriteFDs[SELECT_USE]  = m_WriteFDs[SELECT_BAK];
  37:     m_ExceptFDs[SELECT_USE] = m_ExceptFDs[SELECT_BAK];
  38:  
  39:     _MY_TRY 
  40:     {
  41:         INT iRet = SocketAPI::select_ex(    (INT)m_MaxFD+1 , 
  42:                                             &m_ReadFDs[SELECT_USE] , 
  43:                                             &m_WriteFDs[SELECT_USE] , 
  44:                                             &m_ExceptFDs[SELECT_USE] , 
  45:                                             &m_Timeout[SELECT_USE] ) ; //--------(3)
  46:         if( iRet==SOCKET_ERROR )
  47:         {
  48:             Assert(FALSE) ;
  49:         }
  50:     } 
  51:     _MY_CATCH
  52:     {
  53:         Log::SaveLog( SERVER_LOGFILE, "ERROR: ServerManager::Select( )..." ) ;
  54:     }
  55:  
  56:     return TRUE ;
  57:  
  58: __LEAVE_FUNCTION
  59:  
  60:     return FALSE ;
  61:  
  62: }

 

    如上,在Tick ()函数里首先调用ServerManager::Select (),在Select函数中(2)调用MySleep(50)让出CPU时间片50ms,然后给select_ex设置100us的超时,可以认为每次执行完处理后,会固定sleep 50ms。

    再来看看Threadpool里线程的tick函数

 

   1: BOOL Scene::Tick( )
   2: {
   3:     ........    
   4:  
   5:     //网络处理
   6:     _MY_TRY
   7:     {
   8:         ret = m_pScenePlayerManager->Select( ) ; //--------(1)
   9:         Assert( ret ) ;
  10:     }
  11:     _MY_CATCH
  12:     {
  13:         SaveCodeLog( ) ;
  14:     }
  15:  
  16:     ........
  17: }
  18:                     |
  19:                     |
  20:                    \|/
  21: BOOL ScenePlayerManager::Select( )
  22: {
  23:     {
  24:         MySleep( 50 ) ;          //--------(2)
  25:     }
  26:  
  27:     if( m_MaxFD==INVALID_SOCKET && m_MinFD==INVALID_SOCKET )
  28:         return TRUE ;
  29:  
  30:     m_Timeout[SELECT_USE].tv_sec  = m_Timeout[SELECT_BAK].tv_sec;
  31:     m_Timeout[SELECT_USE].tv_usec = m_Timeout[SELECT_BAK].tv_usec;
  32:  
  33:     m_ReadFDs[SELECT_USE]   = m_ReadFDs[SELECT_BAK];
  34:     m_WriteFDs[SELECT_USE]  = m_WriteFDs[SELECT_BAK];
  35:     m_ExceptFDs[SELECT_USE] = m_ExceptFDs[SELECT_BAK];
  36:  
  37:     _MY_TRY 
  38:     {
  39:         INT ret = SocketAPI::select_ex(    (INT)m_MaxFD+1 , 
  40:                                         &m_ReadFDs[SELECT_USE] , 
  41:                                         &m_WriteFDs[SELECT_USE] , 
  42:                                         &m_ExceptFDs[SELECT_USE] , 
  43:                                         &m_Timeout[SELECT_USE] ) ; //--------(3)
  44:         if( ret == SOCKET_ERROR )
  45:         {
  46:             Assert(FALSE) ;
  47:         }
  48:     } 
  49:     _MY_CATCH
  50:     {
  51:         SaveCodeLog( ) ;
  52:     }
  53:  
  54:     ........   
  55: }

   

    根据以上代码中的(1)、(2)、(3)可以看到场景的tick函数中采用同样的方法,每次调用时先调用MySleep(50)让出CPU时间片50ms,然后再执行相应的处理代码。

    总结

(a)固定Sleep时间在高负荷情况下,由于每次都会强制让出CPU时间片,会不会导致响应不及时?

(b)在每次运行执行处理函数的时间比较稳当的情况下,这种策略还是能提供比较稳定的tick间隔。

(c)这样的设计目前为止本人还没有看出有什么好处?请各位博友指教。

 

 

二、按照物理位置分类

    按照物理位置分类,分为两种,一种是运行在同一物理主机上,另一种是运行在不同的物理主机上。该分类是受到云风一篇blog的启发(见《心跳服务器》)。

(1)心跳服务和逻辑程序运行在同一物理主机上:据我所知,大部分的网游服务器程序都采用这种方式,而且心跳服务往往和逻辑程序写在同一个进程里。心跳服务和逻辑程序不在一个进程里,但运行在同一物理主机上的情况本人没有见过,也不敢妄加臆断。

(2)心跳服务运行在一台独立的物理主机上:(a)按照云风的说法,这样做最大的好处就是方便实现服务器的快放慢放功能,从而很容易重现bug。博文中指出使用使用10Hz的心跳间隔,以局域网内0.1~0.2ms的ping值,不知道能不能容忍?而且使用TCP连接,多两次网络IO,不知道效率如何?(b)另一种独立的心跳服务器是本人读研时参与过的大屏幕显示同步时遇到的,当时一块大屏幕有24台主机,每台主机负责绘制图像中的一块区域。使用一台单独的心跳服务器,用同步卡向着24台主机的串口周期性的发信号,这24台主机收到从串口来的心跳信号后,同时开始绘制这一帧。不过据说精确度有待提高………

 

 

三、总结

    综上所述,最常见的心跳服务是这样设计的:使用固定tick时间策略,与逻辑处理程序写在同一个进程里,tick时间50ms,100ms。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/444960.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

算法(25)-括号

各种括号1.LeetCode-22 括号生成--各种括号排列组合2.LeetCode-20 有效括号(是否)--堆栈3.LeetCode-32 最长有效括号(长度)--dp4.LeetCode-301删除无效括号 --多种删除方式1.LeetCode-22 括号生成–各种括号排列组合 数字 n 代表生成括号的对数&#xff0c;请你设计一个函数&a…

leetcode542 01矩阵

给定一个由 0 和 1 组成的矩阵&#xff0c;找出每个元素到最近的 0 的距离。 两个相邻元素间的距离为 1 。 示例 1: 输入: 0 0 0 0 1 0 0 0 0 输出: 0 0 0 0 1 0 0 0 0 示例 2: 输入: 0 0 0 0 1 0 1 1 1 输出: 0 0 0 0 1 0 1 2 1 注意: 给定矩阵的元素个数不超过 10000。…

RPC、RMI与MOM与组播 通信原理 .

远程过程调用&#xff08;RPC&#xff09;&#xff1a; 即对远程站点机上的过程进行调用。当站点机A上的一个进程调用另一个站点机上的过程时&#xff0c;A上的调用进程挂起&#xff0c;B上的被调用过程执行&#xff0c;并将结果返回给调用进程&#xff0c;使调用进程继续执行【…

一个简单的游戏服务器框架 .

最近一段时间不是很忙&#xff0c;就写了一个自己的游戏服务器框架雏形&#xff0c;很多地方还不够完善&#xff0c;但是基本上也算是能够跑起来了。我先从上层结构说起&#xff0c;一直到实现细节吧&#xff0c;想起什么就写什么。 第一部分 服务器逻辑 服务器这边简单的分为三…

leetcode97 交错字符串

给定三个字符串 s1, s2, s3, 验证 s3 是否是由 s1 和 s2 交错组成的。 示例 1: 输入: s1 "aabcc", s2 "dbbca", s3 "aadbbcbcac" 输出: true 示例 2: 输入: s1 "aabcc", s2 "dbbca", s3 "aadbbbaccc" 输…

leetcode 33 搜索旋转排序数组 到处是细节的好题

这个题想了想就会做&#xff0c;只是细节真的能卡死人&#xff0c;找了好久的bug。甚至我怀疑我现在的代码可能还有错&#xff0c;只是没例子测出来。 假设按照升序排序的数组在预先未知的某个点上进行了旋转。 ( 例如&#xff0c;数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1…

MachineLearning(8)-PCA,LDA基础+sklearn 简单实践

PCA,LDA基础sklearn 简单实践1.PCAsklearn.decomposition.PCA1.PCA理论基础2.sklearn.decomposition.PCA简单实践2.LDAsklearn.discriminant_analysis.LinearDiscriminantAnalysis2.1 LDA理论基础2.2 sklearn LDA简单实践1.PCAsklearn.decomposition.PCA 1.PCA理论基础 PCA:&…

leetcode198 打家劫舍

你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋。每间房内都藏有一定的现金&#xff0c;影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统&#xff0c;如果两间相邻的房屋在同一晚上被小偷闯入&#xff0c;系统会自动报警。 给定一个代表每个房屋存放金额的…

linux下的RPC

一、概述 在传统的编程概念中&#xff0c;过程是由程序员在本地编译完成&#xff0c;并只能局限在本地运行的一段代码&#xff0c;也即其主程序和过程之间的运行关系是本地调用关系。因此这种结构在网络日益发展的今天已无法适应实际需求。总而言之&#xff0c;传统过程调用模式…

算法(28)--矩阵搜索系列

矩阵搜索1.leetcode-200. 岛屿数量2.leetcode-695. 岛屿的最大面积3.leetcode-463. 岛屿的周长4.剑指 Offer 12. 矩阵中的路径5.leetcode-329. 矩阵中的最长递增路径6.leetcode-1091. 二进制矩阵中的最短路径1.leetcode-200. 岛屿数量 给你一个由 ‘1’&#xff08;陆地&#…

leetcode213 打家劫舍II

你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋&#xff0c;每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈&#xff0c;这意味着第一个房屋和最后一个房屋是紧挨着的。同时&#xff0c;相邻的房屋装有相互连通的防盗系统&#xff0c;如果两间相邻的房屋在同一晚…

PaperNotes(4)-高质量图像生成-CGAN-StackGAN-Lapgan-Cyclegan-Pix2pixgan

cgan,stackgan,lapgan,cyclegan,pix2pixgan1.Conditional GAN1.1简介1.2网络结构与训练1.3特点与用途2.Stack GAN2.1简介2.2网络结构与训练2.3特点与用途3.Lap GAN3.1简介3.2网络结构与训练3.3特点与用途4.Pix2pix GAN4.1 简介4.2 网络结构和训练4.3 特点和用途5.Patch GAN6.Cy…

leetcode206 反转链表

反转一个单链表。 示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL 进阶: 你可以迭代或递归地反转链表。你能否用两种方法解决这道题&#xff1f; 经典题不解释 /*** Definition for singly-linked list.* public class ListNode…

leetcode 152 乘积最大子序列

给定一个整数数组 nums &#xff0c;找出一个序列中乘积最大的连续子序列&#xff08;该序列至少包含一个数&#xff09;。 示例 1: 输入: [2,3,-2,4] 输出: 6 解释: 子数组 [2,3] 有最大乘积 6。 示例 2: 输入: [-2,0,-1] 输出: 0 解释: 结果不能为 2, 因为 [-2,-1] 不是子…

PaperNotes(5)-Conditional Generative Adversarial Nets

Conditional GAN 论文阅读笔记Abstract1 Introduction2 Related Work3 Conditional Adversarial Nets3.1 Generative Adversarial Nets3.2 Conditional Adversarial Nets4 Experimental Results4.1 Unimodal4.2 Multimodal5 Future Work6.思考文章地址&#xff1a;https://arxi…

蛙泳姿势教学

偶尔看到分享的一篇日志&#xff0c;记录下&#xff0c;忙过这段时间努力学蛙泳。 蛙泳配合有一个顺口溜&#xff0c;在讲解蛙泳动作要领之前先介绍给大家&#xff1a;“划手腿不动&#xff0c;收手再收腿&#xff0c;先伸胳膊后蹬腿&#xff0c;并拢伸直漂一会儿。”从顺口溜中…

leetcode238 除本身以外数组的乘积

给定长度为 n 的整数数组 nums&#xff0c;其中 n > 1&#xff0c;返回输出数组 output &#xff0c;其中 output[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。 示例: 输入: [1,2,3,4] 输出: [24,12,8,6] 说明: 请不要使用除法&#xff0c;且在 O(n) 时间复杂度内完…

C++(2)--mac使用VScode 进行C++编译、运行、调试

mac 使用VScode 进行C开发1.编译的基础概念2. mac 编译c代码2.1 查看编译器情况2.2 安装插件C/C&#xff0c;C/C Clang Command Adapte2.3新建一个C project2.3.1本地新建文件夹2.3.2新建mian.cpp文件2.3.3 编写hello word demo2.4 代码编译&#xff0c;运行&#xff0c;调试2.…

leetcode136 只出现一次的数字

给定一个非空整数数组&#xff0c;除了某个元素只出现一次以外&#xff0c;其余每个元素均出现两次。找出那个只出现了一次的元素。 说明&#xff1a; 你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗&#xff1f; 示例 1: 输入: [2,2,1] 输出: 1 示例 2: …

leetcode94 二叉树的中序遍历

给定一个二叉树&#xff0c;返回它的中序 遍历。 示例: 输入: [1,null,2,3] 1 \ 2 / 3 输出: [1,3,2] 进阶: 递归算法很简单&#xff0c;你可以通过迭代算法完成吗&#xff1f; 递归 /*** Definition for a binary tree node.* public class TreeNode …