zoukankan      html  css  js  c++  java
  • 并发编程&数据库

    第八章主要内容

    第八章:线程、进程、队列、IO多路模型
    
    操作系统工作原理介绍、线程、进程演化史、特点、区别、互斥锁、信号、事件、join、GIL、进程间通信、管道、队列。
    生产者消息者模型、异步模型、IO多路复用模型、selectpollepoll 高性能IO模型源码实例解析、高并发FTP server开发
    

    一、问答题

    1、简述计算机操作系统中的“中断”的作用?

    中断是指在计算机执行期间,系统内发生任何非寻常的或非预期的急需处理事件,使得cpu暂时中断当前正在执行的程序,
    转去执行相应的事件处理程序。待处理完毕后又返回原来被中断处继续执行或调度新的进程执行的过程。
    它使计算机可以更好更快利用有限的系统资源解决系统响应速度和运行效率的一种控制技术。实时响应,系统调度。
    

    2、简述计算机内存中的“内核态”和“用户态”;

    内核态:cpu可以访问内存的所有数据,包括外围设备,例如硬盘,网卡,cpu也可以将自己从一个程序切换到另一个程序。
    用户态:只能受限的访问内存,且不允许访问外围设备,占用cpu的能力被剥夺,cpu资源可以被其他程序获取。
    为什么要有用户态和内核态?
        由于需要限制不同的程序之间的访问能力,防止他们获取别的程序的内存数据,或者获取外围设备的数据,并发送到网络,
        cpu划分出两个权限等级:用户态和内核态。
    

    3、什么是进程?

    正在执行的一个程序或者说一个任务,负责执行任务的是cpu。进程是用来把资源集中到一起的,进程是资源单位,或者说资源集合。
    

    4、什么是线程?

    线程是cpu上的执行单位。同一个进程内的多个线程共享该进程内的地址资源。创建线程的开销要远小于创建进程的开销。
    

    5、简述程序的执行过程;

    python hello.py
    1.激活了python的解释器,有一个解释器级别的垃圾回收线程(GIL锁)。
    2.一个进程下的多个线程先访问到解释器的代码,多线程去枪GIL锁,抢到的将程序当作参数传递给解释器去执行。
    3.保护不同的数据应该用不同的锁。
    4.python程序是顺序执行的!
    5.一段python程序以.py文件运行时,文件属性__name__==__main__;作为模块导入时,文件属性__name__为文件名。
    

    6、什么是“系统调用”?

    所有用户程序都是运行在用户态的,但是有时候程序确实需要做一些内核态的事情,例如从硬盘读取数据,或者从键盘获取输入等,
    而唯一能做这些事情的就是操作系统,所以此时程序就需要向操作系统请求以程序的名义来执行这些操作。
    这时,就需要一个机制:用户态程序切换到内核态,但是不能控制在内核态中执行的指令。这种机制就叫系统调用。
    

    7、threading模块event和condition的区别;

    condition: 某些事件触发或达到特定的条件后才处理数据,默认创建了一个lock对象。
        con = threading.Condition()
        con.acquire()
        con.notify()
        con.wait()
        con.release()
    event:其他线程需要通过判断某个线程的状态来确定自己的下一步操作,就可以用event。
        from threading import Event
        event = Event()
        event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;
        event.is_set():返回event的状态值;
        event.wait():如果 event.is_set()==False将阻塞线程;
        event.clear():恢复event的状态值为False。
    

    8、进程间通信方式有哪些?

    消息队列 管道 信号量 信号 共享内存 套接字
    

    9、简述你对管道、队列的理解;

    队列 = 管道 + 锁
    from multiprocessing import Queue,Process
        queue = Queue()
        queue.put(url)
        url = queue.get()
    from multiprocessing import Pipe,Process
        pipe = Pipe()
        pipe.send(url)
        pipe.recv() 
    

    10、请简述你对join、daemon方法的理解,举出它们在生产环境中的使用场景;

    join: 等待一个任务执行完毕;可以将并发变成串行。
    daemon: 
        守护进程(守护线程)会等待主进程(主线程)运行完毕后被销毁。
        运行完毕:
            1.对主进程来说,运行完毕指的是主进程代码运行完毕。
            2.对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕。
    

    11、请简述IO多路复用模型的工作原理;

    IO多路复用实际上就是用select,poll,epoll监听多个io对象,当io对象有变化(有数据)的时候就通知用户进程。好处就是单个进程可以处理多个socket。
    1.当用户进程调用了select,那么整个进程会被block;
    2.而同时,kernel会“监视”所有select负责的socket;
    3.当任何一个socket中的数据准备好了,select就会返回;
    4.这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
    总结:
        1.I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。
        2.IO多路复用:需要两个系统调用,system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。
        3.如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用多线程 + 阻塞 IO的web server性能更好,可能延迟还更大。
        4.select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
    

    12、threading中Lock和RLock的相同点和不同点;

    Lock():互斥锁,只能被acquire一次,可能会发生死锁情况。 
    RLock():递归锁,可以连续acquire多次。
    RLock = Lock + counter
        counter:记录了acquire的次数,直到一个线程所有的acquire都被release,其他线程才能获得资源。 
    

    13、什么是select,请简述它的工作原理,简述它的优缺点;

    python中的select模块专注于I/O多路复用,提供了select poll epoll三个方法;后两个在linux中可用,windows仅支持select。
        fd:文件描述符
        fd_r_list,fd_w_list,fd_e_list = select.select(rlist,wlist,xlist,[timeout])
        参数:可接受四个参数(前三个必须)
        rlist:等到准备好阅读
        wlist:等到准备写作
        xlist:等待“异常情况”
        超时:超时时间
        返回值:三个列表
    select监听fd变化的过程分析:
        用户进程创建socket对象,拷贝监听的fd到内核空间,每一个fd会对应一张系统文件表,内核空间的fd响应到数据后,
        就会发送信号给用户进程数据已到;
        用户进程再发送系统调用,比如(accept)将内核空间的数据copy到用户空间,同时作为接受数据端内核空间的数据清除,
        这样重新监听时fd再有新的数据又可以响应到了(发送端因为基于TCP协议所以需要收到应答后才会清除)。
    该模型的优点:
        相比其他模型,使用select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时能够为多客户端提供服务。
        如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值。
    该模型的缺点:
        首先select()接口并不是实现“事件驱动”的最好选择。因为当需要探测的句柄值较大时,select()接口本身需要消耗大量时间去轮询各个句柄。
        很多操作系统提供了更为高效的接口,如linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll,…。
        如果需要实现更高效的服务器程序,类似epoll这样的接口更被推荐。遗憾的是不同的操作系统特供的epoll接口有很大差异,
        所以使用类似于epoll的接口实现具有较好跨平台能力的服务器会比较困难。
        其次,该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的。    
    

    14、什么是epoll,请简述它的工作原理,简述它的优缺点;

    epoll: 性能最好的多路复用I/O就绪通知方法。相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。
           因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。
    epoll:同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,
           你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
           另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,
           而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,
           当进程调用epoll_wait()时便得到通知。从以上可知,epoll是对select、poll模型的改进,提高了网络编程的性能,广泛应用于大规模并发请求的C/S架构中。
    python中的epoll: 
        只适用于unix/linux操作系统
    

    15、简述select和epoll的区别;

    select: 调用select()时
      1、上下文切换转换为内核态
      2、将fd从用户空间复制到内核空间
      3、内核遍历所有fd,查看其对应事件是否发生
      4、如果没发生,将进程阻塞,当设备驱动产生中断或者timeout时间后,将进程唤醒,再次进行遍历
      5、返回遍历后的fd
      6、将fd从内核空间复制到用户空间
    select: 缺点
        1、当文件描述符过多时,文件描述符在用户空间与内核空间进行copy会很费时
      2、当文件描述符过多时,内核对文件描述符的遍历也很浪费时间
      3、select最大仅仅支持1024个文件描述符
    epoll很好的改进了select:
      1、epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时,会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。
      2、epoll会在epoll_ctl时把指定的fd遍历一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表。
           epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd。
      3、epoll对文件描述符没有额外限制。
    

    16、简述多线程和多进程的使用场景;

    多进程用于计算密集型,如金融分析;利用多核实现并发。
    多线程用于IO密集型,如socket,爬虫,web。
    

    17、请分别简述threading.Condition、threading.event、threading.semaphore的使用场景;

    condition: 某些事件触发或达到特定的条件后才处理数据。
    event: 用来通知线程有一些事情已发生,从而启动后继任务的开始。
    semaphore: 为控制一个具有有限数量用户资源而设计。
    

    18、假设有一个名为threading_test.py的程序里有一个li = [1, 2, 3, 4]的列表,另有a,b两个函数分别往该列表中增加元素,a函数需要修改li之前需要获得threading.Lock对象,b函数不需要,请问当线程t1执行a函数获取到Lock对象之后并没有release该对象的情况下,线程t2执行b函是否可以修改li,为什么?

    可以,线程的数据是共享的,a 函数虽然上了锁,没有释放。由于b 函数不需要上锁,就可以访问资源。
    

    19、简述你对Python GIL的理解;

    GIL(global interpreter lock)全局解释器锁
    GIL是CPython的一个概念,本质是一把互斥锁,将并发运行变成串行。
    解释器的代码是所有线程共享的,所以垃圾回收线程也有可能访问到解释器的代码去执行。
    因此需要有GIL锁,保证python解释器同一时间只能执行一个任务的代码。
    GIL:解释器级别的锁(保护的是解释器级别的数据,比如垃圾回收的数据)
    Lock:应用程序的锁(保护用户自己开发的应用程序的数据)
    

    20、请列举你知道的进程间通信方式;

    消息队列 管道 信号量 信号 共享内存 套接字
    

    21、什么是同步I/O,什么是异步I/O?

    synchronous io: 做”IO operation”的时候会将process阻塞;”IO operation”是指真实的IO操作
                    blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO这一类.
    asynchronous io: 当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,
                     告诉进程说IO完成。在这整个过程中,进程完全没有被block。异步io的实现会负责把数据从内核拷贝到用户空间。
    

    22、什么是管道,如果两个进程尝试从管道的同一端读写数据,会出现什么情况?

    管道:是两个进程间进行单向通信的机制。由于管道传递数据的单向性。管道又称为半双工管道。
          管道传递数据是单向性的,读数据时,写入管道应关闭。写数据时,读取管道应关闭。
    

    23、为什么要使用线程池/进程池?

    对服务端开启的进程数或线程数加以控制,让机器在一个自己可以承受的范围内运行,这就是进程池或线程池的用途.
    

    24、如果多个线程都在等待同一个锁被释放,请问当该锁对象被释放的时候,哪一个线程将会获得该锁对象?

    这个由操作系统的调度决定。
    

    25、import threading;s = threading.Semaphore(value=-1)会出现什么情况?

    当threading.Semaphore(1) 为1时,表示只有一个线程能够拿到许可,其他线程都处于阻塞状态,直到该线程释放为止。
    当然信号量不可能永久的阻塞在那里。信号量也提供了超时处理机制。如果传入了 -1,则表示无限期的等待。
    

    26、请将二进制数10001001转化为十进制;

    10001001 = 1*10^7 + 1*10^3 + 1* 10^0 = 10001001 
    

    27、某进程在运行过程中需要等待从磁盘上读入数据,此时该进程的状态将发生什么变化?

    一个程序有三种状态:运行态,阻塞态,就绪态;
    遇到IO阻塞,进程从运行态转到阻塞态,cpu切走,保存当前状态;
    

    28、请问selectors模块中DefaultSelector类的作用是什么;

    IO多路复用:select poll epoll
        select: 列表循环,效率低。windows 支持。
        poll: 可接收的列表数据多,效率也不高。linux 支持。
        epoll: 效率最高 异步操作 + 回调函数。linux 支持。
    selectors 模块:
        sel=selectors.DefaultSelector()
        自动根据操作系统选择select/poll/epoll
    

    29、简述异步I/O的原理;

    用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,
    首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,
    当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。 
    

    30、请问multiprocessing模块中的Value、Array类的作用是什么?举例说明它们的使用场景

    python 多进程通信Queue Pipe Value Array
    queue和pipe用来在进程间传递消息;
    Value + Array 是python中共享内存映射文件的方法;速度比较快.
    

    31、请问multiprocessing模块中的Manager类的作用是什么?与Value和Array类相比,Manager的优缺点是什么?

    Python multiprocessing.Manager(进程间共享数据)
    Python中进程间共享数据,除了基本的queue,pipe和value+array外,还提供了更高层次的封装。使用multiprocessing.Manager可以简单地使用这些高级接口。
    Manager支持的类型有list,dict,Namespace,Lock,RLock,Semaphore,BoundedSemaphore,Condition,Event,Queue,Value和Array。
    

    32、请说说你对multiprocessing模块中的Queue().put(), Queue().put_nowait(), Queue().get(), Queue().get_nowait()的理解;

    q = Queue(3) 队列 先进先出  进程间通信; 队列 = 管道 + 锁
    q.put() 
    q.put_nowait()  # 无阻塞,当队列满时,直接抛出异常queue.Full
    q.get() 
    q.get_nowait()  # 无阻塞,当队列为空时,直接抛出异常queue.Empty 
    

    33、什么是协程?使用协程与使用线程的区别是什么?

    协程:单线程下的并发。协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。
        1.python的线程是属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他的线程运行)
        2.单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(!!非io操作的切换与效率无关)
    

    34、asyncio的实现原理是什么?

    https://www.cnblogs.com/earendil/p/7411115.html 
    Python异步编程:asyncio库和async/await语法
    asyncio是Python 3.4 试验性引入的异步I/O框架,提供了基于协程做异步I/O编写单线程并发代码的基础设施。
    其核心组件有事件循环(Event Loop)、协程(Coroutine)、任务(Task)、未来对象(Future)以及其他一些扩充和辅助性质的模块。
    
    synchronous io: 做”IO operation”的时候会将process阻塞;”IO operation”是指真实的IO操作
                    blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO这一类.
    asynchronous io: 当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,
                     告诉进程说IO完成。在这整个过程中,进程完全没有被block。异步io的实现会负责把数据从内核拷贝到用户空间。
    

    二、编程题

    1、请写一个包含10个线程的程序,主线程必须等待每一个子线程执行完成之后才结束执行,每一个子线程执行的时候都需要打印当前线程名、当前活跃线程数量;

    from threading import Thread,currentThread,activeCount
    import time
    def task(n):
        print('线程名:%s----%s'%(currentThread().name,n))
        time.sleep(1)
        print('数量:%s'%activeCount())
        
    if __name__ == "__main__":
        t_li = []
        for i in range(10):
            t = Thread(target=task,args=(i,))
            t.start()
            t_li.append(t)
        for t in t_li:
            t.join()
            
        print('主,end----')
    

    2、请写一个包含10个线程的程序,并给每一个子线程都创建名为"name"的线程私有变量,变量值为“Alex”;

    from threading import Thread
    def task(name):
        print('%s is running'%name)
        print('end ---')
        
    if __name__ == "__main__":
        for i in range(10):
            t = Thread(target=task,args=('alex_%s'%i,))
            t.start()
            
        print('主 end ---')
    

    3、请使用协程写一个消费者生产者模型;

    def consumer():
        while True:
            x = yield
            print('消费:', x)
            
    def producter():
        c = consumer()
        next(c)
        for i in range(10):
            print('生产:', i)
            c.send(i)
            
    producter()
    

    4、写一个程序,包含十个线程,子线程必须等待主线程sleep 10秒钟之后才执行,并打印当前时间;

    from threading import Thread,Event
    import time
    import datetime
    def task():
        # while not event.is_set():
        #     print('...')
        print('...')
        event.wait(10)
        print('time:',datetime.datetime.now())
        
    if __name__ == '__main__':
        event = Event()
        for i in range(10):
            t = Thread(target=task)
            t.start()
            
        time.sleep(10)
        event.set()
    

    5、写一个程序,包含十个线程,同时只能有五个子线程并行执行;

    from threading import Thread,Semaphore,currentThread
    import time
    def task(n):
        sm.acquire()
        print('%s---'%n,currentThread().name)
        time.sleep(1)
        print('end----')
        sm.release()
        
    if __name__ == '__main__':
        sm = Semaphore(5)
        for i in range(10):
            t = Thread(target=task,args=(i,))
            t.start()
    

    6、写一个程序 ,包含一个名为hello的函数,函数的功能是打印字符串“Hello, World!”,该函数必须在程序执行30秒之后才开始执行(不能使用time.sleep());

    from threading import Timer
    def hello(name):
        print('%s say '%name,'Hello World!')
        
    if __name__ == "__main__":
        t = Timer(5,hello,args=('alice',))
        t.start()
    

    7、写一个程序,利用queue实现进程间通信;

    from multiprocessing import Process,Queue,current_process
    import time
    def consumer(q):
        while True:
            res = q.get()
            if not res:break
            print('消费了:',res,'--',current_process().name)
            
    def producter(q):
        for i in range(5):
            print('生产:',i)
            time.sleep(1)
            q.put(i)
            
    if __name__ == "__main__":
        q = Queue()
        p1 = Process(target=producter,args=(q,))
        c1 = Process(target=consumer,args=(q,))
        c2 = Process(target=consumer,args=(q,))
        p1.start()
        c1.start()
        c2.start()
      
        p1.join()
        q.put(None)
        q.put(None)
        print('主')
        
    # JoinableQueue
    from multiprocessing import Process,JoinableQueue,current_process
    import time
    def consumer(q):
        while True:
            res = q.get()
            print('消费了:',res,'--',current_process().name)
            q.task_done()
            
    def producter(q):
        for i in range(5):
            print('生产:',i,'--',current_process().name)
            time.sleep(1)
            q.put(i)
        q.join()
        
    if __name__ == "__main__":
        q = JoinableQueue()
        p1 = Process(target=producter,args=(q,))
        p2 = Process(target=producter, args=(q,))
        c1 = Process(target=consumer,args=(q,))
        c2 = Process(target=consumer,args=(q,))
        p1.start()
        p2.start()
        
        c1.daemon = True
        c2.daemon = True
        c1.start()
        c2.start()
        
        p1.join()
        p2.join()
        print('主')
    

    8、写一个程序,利用pipe实现进程间通信;

    from multiprocessing import Process,Pipe
    def task(conn):
        conn.send('hello world')
        conn.close()
        
    if __name__ == "__main__":
        parent_conn,child_conn = Pipe()
        p = Process(target=task,args=(child_conn,))
        p.start()
        p.join()
        print(parent_conn.recv())
    

    9、使用selectors模块创建一个处理客户端消息的服务器程序;

    # server  blocking IO  
    import socket
    server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    server.bind(('127.0.0.1',8080))
    server.listen(5)
    while True:
        conn,addr = server.accept()
        print(addr)
        while True:
            try:
                data = conn.recv(1024)
                if not data: break
                conn.send(data.upper())
            except Exception as e:
                print(e)
                break
                
    # server  IO多路复用 selectors 会根据操作系统选择select poll epoll           
    import socket
    import selectors
    sel = selectors.DefaultSelector()
    def accept(server_fileobj,mask):
        conn,addr = server_fileobj.accept()
        print(addr)
        sel.register(conn,selectors.EVENT_READ,read)
        
    def read(conn,mask):
        try:
            data = conn.recv(1024)
            if not data:
                print('closing..',conn)
                sel.unregister(conn)
                conn.close()
                return
            conn.send(data.upper())
        except Exception:
            print('closeing...',conn)
            sel.unregister(conn)
            conn.close()
            
    server_fileobj = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    server_fileobj.bind(('127.0.0.1',8080))
    server_fileobj.listen(5)
    server_fileobj.setblocking(False)
    sel.register(server_fileobj,selectors.EVENT_READ,accept)
    while True:
        events = sel.select()
        for sel_obj,mask in events:
            callback = sel_obj.data
            callback(sel_obj.fileobj,mask)
            
    # client
    # -*- coding:utf-8 -*-
    import socket
    client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    client.connect(('127.0.0.1',8080))
    while True:
        msg = input('>>>:').strip()
        if not msg:continue
        client.send(msg.encode('utf-8'))
        data = client.recv(1024)
        print(data.decode('utf-8'))
    

    10、使用socketserver创建服务器程序时,如果使用fork或者线程服务器,一个潜在的问题是,恶意的程序可能会发送大量的请求导致服务器崩溃,请写一个程序,避免此类问题;

    # server socketserver 模块内部使用IO多路复用 和多进程/多线程
    import socketserver
    class Handler(socketserver.BaseRequestHandler):
        def handle(self):
            print('new connection:',self.client_address)
            while True:
                try:
                    data = self.request.recv(1024)
                    if not data:break
                    print('client data:',data.decode())
                    self.request.send(data.upper())
                except Exception as e:
                    print(e)
                    break
                    
    if __name__ == "__main__":
        server = socketserver.ThreadingTCPServer(('127.0.0.1',8080),Handler)
        server.serve_forever()
    

    11、请使用asyncio实现一个socket服务器端程序;

    ...
    ...
    

    第九章主要内容

    1、数据库介绍、类型、特性
    2、MySQL数据库安装、连接、启动、停止
    3、表字段类型介绍、主键约束、表创建语句
    4、常用增删改查语句、分组、聚合
    5、外键管理、unique字段、表结构修改语法
    6、跨表查询,inner join、left join、right join、full join语法
    7、复杂SQL语句如group by、子查询、函数的使用
    8、索引原理及作用、普通索引、多列索引、唯一索引、全文索引等
    9、基于hash&b+树索引的实现原理,索引的优缺点剖析
    10、事务原理,ACID特性,应用场景讲解
    11、事务回滚
    12、触发器的特性,应用场景
    13、触发器的增删改查方法
    14、存储过程的作用及应用场景
    15、创建存储过程,参数传递,流程控制语句ifwhile
    epeatloop等,动态SQL的创建
    16、视图的作用及使用场景,视图的增删改查
    17、数据库权限管理,用户管理
    18、数据库备份命令及工具讲解
    19、基于不同业务的数据库表结构设计、性能优化案例
    20、pymysql模块介绍和使用
    

    一、问答题

    1、说说你所知道的MySQL数据库存储引擎,InnoDB存储引擎和MyISM存储引擎的区别?

    主要有
    MyISM:MyISAM存储引擎:不支持事务、也不支持外键,优势是访问速度快,对事务完整性没有 要求或者以select,insert为主的应用基本上可以用这个引擎来创建表
    InnoDB:支持事务
    Memory:Memory存储引擎使用存在于内存中的内容来创建表。每个memory表只实际对应一个磁盘文件,格式是.frm。memory类型的表访问非常的快,因为它的数据是放在内存中的,并且默认使用HASH索引,但是一旦服务关闭,表中的数据就会丢失掉。
    Merge:Merge存储引擎是一组MyISAM表的组合,这些MyISAM表必须结构完全相同,merge表本身并没有数据,对merge类型的表可以进行查询,更新,删除操作,这些操作实际上是对内部的MyISAM表进行的。
    BLACKHOLE:黑洞存储引擎,可以应用于主备复制中的分发主库。
    
    MyISM和InnoDB的区别
    InnoDB支持事务,而MyISM不支持事务
    InnoDB支持行级锁,而MyISM支持表级锁
    InnoDB支持外键,而MyISM不支持
    InnoDB支持全文索引,而MyISM不支持
    InnoDB是索引组织表,MyISM是堆表  (堆表的数据是随机插入的,索引组织表的数据是有序的)
    

    2、MySQL中char和varchar的区别,varchar(50)和char(50)分别代表什么意思?

    char(50): 定长,字符的长度为50,浪费空间,存取速度快,数据不足时,会往右填充空格来满足长度。
    varchar(50): 变长,字符的长度为50,节省空间,存取速度慢,存储数据的真实内容,不会填充空格,
                 且会在真实数据前加1-2bytes,表示真实数据的bytes字节数。
    

    3、MySQL中int类型存储多少个字节?

    int存储4字节  有符号:(-2147483648,2147483647)
                  无符号:(0,4294967295)
    

    4、主键具有什么特征?

    不为空为唯一
    

    5、简述你对inner join、left join、right join、full join的理解;

    多表连接查询:
    inner join: 内连接,只连接匹配的行,找两张表共有的部分;
    left join:   外连接之左连接,优先显示左表全部记录,在内连接的基础上增加左表有右表没有的结果;
    right join:  外连接之右连接,优先显示右表全部记录,在内连接的基础上增加右表有左表没有的结果;
    full join:   = left join on union right join on ...  mysql 不支持full join 但是可以用 union ...
                 全外连接,显示左右两个表全部记录,在内连接的基础上增加左表有右表没有和右表有左表没有的结果; 
    

    6、concat, group_concat函数的作用是什么?

    定义显示格式:
        concat() 用于连接字符串 
            eg: select concat('姓名:',name,'年薪:',salasy*12) as annual_salary from employee;
        concat_ws() 第一个参数为分隔符
            eg: select concat_ws(':',name,salary*12) as annual_salary from employee;
    group by 与 group_concat() 函数一起使用
        select post,group_concat(name) as emp_members from employee group by post;     
    

    7、请介绍事务的实现原理;

    事务:用于将某些操作的多个sql作为原子性操作,一旦有某一个出现错误,即可回滚到原来的状态,从而保证数据库数据的完整性。
          原子性:一堆sql语句,要么同时执行成功,要么同时失败!
    

    8、索引的本质是什么?索引有什么优点,缺点是什么?

    索引是帮助MySQL高效获取数据的数据结构。因此,索引的本质是一种数据结构。
    在数据之外,数据库系统还可以维护满足特定查找算法的数据结构,这些数据结构以某种方式指向真实数据,这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。
    
    优点:
    1、提高数据检索效率,降低数据库的IO成本;
    2、通过索引对数据进行排序,降低了数据排序的成本,降低了CPU的利用率;
    
    缺点:
    1、索引实际上也是一张表,索引会占用一定的存储空间;
    2、更新数据表的数据时,需要同时维护索引表,因此,会降低insert、update、delete的速度;
    

    9、哪些情况下需要创建索引,哪些情况下不需要创建索引?

    1、主键自动创建唯一非空索引;
    2、频繁作为查询条件的字段应该创建索引;
    3、频繁更新的字段不适合简历索引,因为每次更新不仅仅更新数据表同时还会更新索引表;
    4、查询中经常排序的字段,可以考虑创建索引;
    5、如果某个字段的重复数据较多,不适合创建普通索引;
    

    10、请分别介绍ACID代表的意思,什么业务场景需要支持事务,什么业务场景不需要支持事务?

    ACID,指数据库事务正确执行的四个基本要素的缩写。
    包含:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。
    一个支持事务(Transaction)的数据库,必须要具有这四种特性,否则在事务过程(Transaction processing)当中无法保证数据的正确性。
    使用场景:
        银行的交易系统
    eg:
        start transaction; 
        update user set balance = 900 where name = 'wsb';   #买支付100元
        update user set balance = 1010 where name = 'egon'; #中介拿走10元
        uppdate user set balance = 1090 where name = 'ysb'; #卖家拿到90元,出现异常没有拿到
        rollback;
        commit;
    

    11、什么是触发器,请简述触发器的使用场景?

    使用触发器可以定制用户对表进行【增、删、改】操作时前后的行为,注意:没有查询。
    触发器无法由用户直接调用,而知由于对表的【增/删/改】操作被动引发的。
    eg: 
        create trigger tri_before_insert_tb1 before insert on tb1 for each row
        begin 
            ...      
        end  
    

    12、什么是存储过程,存储过程的作用是什么?

    存储过程包含了一系列可执行的sql语句,存储过程存放于MySQL中,通过调用它的名字可以执行其内部的一堆sql。
    优点:
        1. 用于替代程序写的SQL语句,实现程序与sql解耦
        2. 基于网络传输,传别名的数据量小,而直接传sql数据量大
    缺点:
        1. 程序员扩展功能不方便
    eg:
         delimiter //
            create procedure p1()
            begin
                select * from blog;
                insert into blog(name,sub_time) values('xxx',now());
            end //
         delimiter ;
    

    13、什么是视图,简单介绍视图的作用和使用场景?

    视图是一个虚拟表(非真实存在),其本质是【根据SQL语句获取动态的数据集,并为其命名】,
    用户使用时只需使用【名称】即可获取结果集,可以将该结果集当做表来使用。
    视图取代复杂的sql语句,方便用来查询。
    eg: 
        create view teacher_view as select tid from teacher where tname='李平老师';
    

    14、如何查看SQL语句的执行计划?

    http://blog.itpub.net/12679300/viewspace-1394985/
    执行计划的查看是进行数据库的sql语句调优时依据的一个重要依据.
    eg:
        explain select * from class;
        +----+-------------+-------+------+---------------+------+---------+------+------+-------+
        | id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra |
        +----+-------------+-------+------+---------------+------+---------+------+------+-------+
        |  1 | SIMPLE      | class | ALL  | NULL          | NULL | NULL    | NULL |   12 | NULL  |
        +----+-------------+-------+------+---------------+------+---------+------+------+-------+
    Id:包含一组数字,表示查询中执行select子句或操作表的顺序;
        执行顺序从大到小执行;
        当id值一样的时候,执行顺序由上往下;
    Select_type:表示查询中每个select子句的类型(简单OR复杂),有以下几种:
        SIMPLE:查询中不包含子查询或者UNION
        PRIMARY:查询中若包含任何复杂的子部分,最外层查询则被标记为PRIMARY
        SUBQUERY:在SELECT或WHERE列表中包含了子查询,该子查询被标记为SUBQUERY
        DERIVED:在FROM列表中包含的子查询被标记为DERIVED(衍生)
        若第二个SELECT出现在UNION之后,则被标记为UNION;
        若UNION包含在FROM子句的子查询中,外层SELECT将被标记为:DERIVED
        从UNION表获取结果的SELECT被标记为:UNION RESULT
    Type:表示MySQL在表中找到所需行的方式,又称“访问类型”,常见有以下几种:
        ALL:Full Table Scan, MySQL将进行全表扫描;
        index:Full Index Scan,index与ALL区别为index类型只遍历索引树;
        range:range Index Scan,对索引的扫描开始于某一点,返回匹配值域的行,常见于between、<、>等的查询;
        ref:非唯一性索引扫描,返回匹配摸个单独值的所有行。常见于使用非唯一索引或唯一索引的非唯一前缀进行的查找;
        eq_ref:唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。常见于主键或唯一索引扫描
        const、system:当MySQL对查询某部分进行优化,并转换为一个常量时,使用这些类型访问。如将主键置于where列表中,MySQL就能将该查询转换为一个常量
        NULL:MySQL在优化过程中分解语句,执行时甚至不用访问表或索引
    possible_keys:指出MySQL能使用哪个索引在表中找到行,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用;    
    key:显示MySQL在查询中实际使用的索引,若没有使用索引,显示为NULL。当查询中若使用了覆盖索引,则该索引仅出现在key列表中
    key_len:表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度
    ref:表示上述表的连接匹配条件,即那些列或常量被用于查找索引列上的值;
    rows:表示MySQL根据表统计信息及索引选用情况,估算的找到所需的记录所需要读取的行数;
    Extra:包含不适合在其他列中显示但十分重要的额外信息;
        Using where:表示MySQL服务器在存储引擎受到记录后进行“后过滤”(Post-filter),如果查询未能使用索引,Using where的作用只是提醒我们MySQL将用where子句来过滤结果集
        Using temporary:表示MySQL需要使用临时表来存储结果集,常见于排序和分组查询;
        Using filesort:MySQL中无法利用索引完成的排序操作称为“文件排序”;
    

    15、在你本地数据库中查看select * from student的执行计划,并解释每个字段分别代表什么意思?

    mysql> explain select * from student;
    +----+-------------+---------+------+---------------+------+---------+------+------+-------+
    | id | select_type | table   | type | possible_keys | key  | key_len | ref  | rows | Extra |
    +----+-------------+---------+------+---------------+------+---------+------+------+-------+
    |  1 | SIMPLE      | student | ALL  | NULL          | NULL | NULL    | NULL |   16 | NULL  |
    +----+-------------+---------+------+---------------+------+---------+------+------+-------+
    id: 表示查询中执行select子句或操作表的顺序。
    select_type: simple 表示查询中不包含子查询或者union
    table: student
    type: all 表示mysql将进行全表扫描
    possible_keys: 指出MySQL能使用哪个索引在表中找到行,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用;
    key: 显示MySQL在查询中实际使用的索引,若没有使用索引,显示为NULL。当查询中若使用了覆盖索引,则该索引仅出现在key列表中;
    ey_len:表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度;
    ref:表示上述表的连接匹配条件,即那些列或常量被用于查找索引列上的值;
    rows:表示MySQL根据表统计信息及索引选用情况,估算的找到所需的记录所需要读取的行数;
    Extra:包含不适合在其他列中显示但十分重要的额外信息;
    

    16、数据备份分为哪几种类型?增量备份和差异备份的区别是什么?

    完整备份:备份系统中的所有数据。特点:占用空间大,备份速度慢,但恢复时一次恢复到位,恢复速度快。
    增量备份:只备份上次备份以后有变化的数据。
             特点:因每次仅备份自上一次备份(注意是上一次,不是第一次)以来有变化的文件,
                所以备份体积小,备份速度快,但是恢复的时候,需要按备份时间顺序,逐个备份版本进行恢复,恢复时间长。
    差异备份:只备份上次完全备份以后有变化的数据。
             特点:占用空间比增量备份大,比完整备份小,恢复时仅需要恢复第一个完整版本和最后一次的差异版本,恢复速度介于完整备份和增量备份之间。
    
    简单的讲,完整备份就是不管三七二十一,每次都把指定的备份目录完整的复制一遍,不管目录下的文件有没有变化;
    增量备份就是每次将之前(第一次、第二次、直到前一次)做过备份之后有变化的文件进行备份;
    差异备份就是每次都将第一次完整备份以来有变化的文件进行备份。   
    

    17、请介绍select语句的执行顺序;

    from
    where
    group by
    having
    select
    distinct
    order by
    limit
    说明:
        1.找到表:from
        2.拿着where指定的约束条件,去文件/表中取出一条条记录
        3.将取出的一条条记录进行分组group by,如果没有group by,则整体作为一组
        4.将分组的结果进行having过滤
        5.执行select
        6.去重
        7.将结果按条件排序:order by
        8.限制结果的显示条数
    

    18、请问存储引擎MyISM和InnoDB的适合什么样的使用场景?

    Innodb与Myisam引擎的区别与应用场景:
    1. 区别:
    (1)事务处理:
        MyISAM是非事务安全型的,而InnoDB是事务安全型的(支持事务处理等高级处理);
    (2)锁机制不同:
        MyISAM是表级锁,而InnoDB是行级锁;
    (3)select ,update ,insert ,delete 操作:
        MyISAM:如果执行大量的SELECT,MyISAM是更好的选择
        InnoDB:如果你的数据执行大量的INSERT或UPDATE,出于性能方面的考虑,应该使用InnoDB表
    (4)查询表的行数不同:
        MyISAM:select count(*) from table,MyISAM只要简单的读出保存好的行数,注意的是,当count(*)语句包含   where条件时,两种表的操作是一样的
        InnoDB : InnoDB 中不保存表的具体行数,也就是说,执行select count(*) from table时,InnoDB要扫描一遍整个表来计算有多少行
    (5)外键支持:
        mysiam表不支持外键,而InnoDB支持
        
    2. 为什么MyISAM会比Innodb 的查询速度快。
        INNODB在做SELECT的时候,要维护的东西比MYISAM引擎多很多;
            1)数据块,INNODB要缓存,MYISAM只缓存索引块,  这中间还有换进换出的减少; 
            2)innodb寻址要映射到块,再到行,MYISAM 记录的直接是文件的OFFSET,定位比INNODB要快
            3)INNODB还需要维护MVCC一致;虽然你的场景没有,但他还是需要去检查和维护
            MVCC ( Multi-Version Concurrency Control )多版本并发控制 
    
    3. 应用场景
        MyISAM适合:(1)做很多count 的计算;(2)插入不频繁,查询非常频繁;(3)没有事务。
        InnoDB适合:(1)可靠性要求比较高,或者要求事务;(2)表更新和查询都相当的频繁,并且行锁定的机会比较大的情况。
    

    19、请举出MySQL中常用的几种数据类型;

     mysql常用数据类型:
        1.数值类型:
            整数类型:tinyint smallint int bigint
            浮点型:float double decimal
                float :在位数比较短的情况下不精准(一般float得精确度也够用了)
                double :在位数比较长的情况下不精准
                    0.000001230123123123
                    存成:0.000001230000
                decimal:(如果用小数,则推荐使用decimal)
                    精准 内部原理是以字符串形式去存
        2.日期类型:
            最常用:datetime  year date time datetime timestamp
        3.字符串类型:
            char(6) varchar(6)
            char(10):简单粗暴,浪费空间,存取速度快,定长;
                root存成root000000
            varchar:精准,节省空间,存取速度慢,变长;
            
            sql优化:创建表时,定长的类型往前放,变长的往后放
                            比如性别           比如地址或描述信息
            
            >255个字符,超了就把文件路径存放到数据库中。
                    比如图片,视频等找一个文件服务器,数据库中只存路径或url。
        4.枚举类型与集合类型:
            enum('male','female')
            set('play','music','read','study')
    

    20、什么情况下会产生笛卡尔乘积,如何避免?

    交叉连接:不适用任何匹配条件。生成笛卡尔积;
        select * from employee,department;
    避免:    
        select 
            employee.id,employee.name,employee.age,employee.sex,department.name 
        from 
            employee,department 
        where 
            employee.dep_id=department.id; 
    

    21、请列举MySQL中常用的函数;

    聚合函数:
        聚合函数聚合的是组的内容,若是没有分组,则默认一组
        count()
        max()
        min()
        avg()
        sum()
    

    22、请说明group by的使用场景;

    什么是分组,为什么要分组?
        1、首先明确一点:分组发生在where之后,即分组是基于where之后得到的记录而进行的
        2、分组指的是:将所有记录按照某个相同字段进行归类,比如针对员工信息表的职位分组,或者按照性别进行分组等
        3、为何要分组呢?
            取每个部门的最高工资
            取每个部门的员工数
            取男人数和女人数
    
        小窍门:‘每’这个字后面的字段,就是我们分组的依据
        4、大前提:
            可以按照任意字段分组,但是分组完毕后,比如group by post,只能查看post字段,如果想查看组内信息,需要借助于聚合函数
    

    23、请介绍hash索引和B+树索引的实现原理;

    哈希索引基于哈希表实现,只有精确匹配索引的所有列的查询才有效。对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码,哈希码是一个较小的值,并且不同键值的行计算出来的哈希码也不一样。哈希索引将所有的哈希码存储在索引中,同时在哈希表中保存指向每个数据行的指针。也就是说,由于哈希查找比起B-Tree索引,其本身对于单行查询的时间复杂度更低,有了哈希索引后明显可加快单行查询速度。
    
    但是哈希索引也有它自己的限制:
    哈希索引只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行。不过,访问内存中的行的速度很快,所以大部分情况下这一点对性能的影响并不明显。
    哈希索引数据并不是按照索引值顺序存储的,所以也就无法用于排序。
    哈希索引也不支持部分索引列匹配查找,因为哈希索引始终是使用索引列的全部内容来计算哈希值的。例如,在数据列(A, B)上建立哈希索引,如果查询只有数据列A,则无法使用该索引。
    哈希索引只支持等值比较查询,包括=、in()、<=>。不支持任何范围查询,例如where price > 100。
    访问哈希索引的数据非常快,除非有很多哈希冲突。如果哈希冲突很多的话,一些索引维护操作的代价也很高。
    
    B+树索引是B树索引的变体,本质上也是多路平衡查找树
    

    二、编程题

    1、创建一个表student,包含ID(学生学号),sname(学生姓名),gender(性别),credit(信用卡号),四个字段,要求:ID是主键,且值自动递增,sname是可变长字符类型,gender是枚举类型, credit是可变长字符类型;

    create table student(
        ID int primary key auto_increment,
        sname varchar(16) not null,
        gender enum('male','female') not null default 'female',
        credit varchar(32)
    );
    

    2、在上面的student表中增加一个名为class_id的外键,外键引用class表的cid字段;

    create table class(
        cid int primary key auto_increment,
        cname varchar(16) not null
    );
      
    alter table student add class_id int not null;
    alter table student add foreign key(class_id) references class(cid) on delete cascade on update cascade;
    

    3、向该表新增一条数据,ID为1,学生姓名为alex,性别女,修改ID为1的学生姓名为wupeiqi,删除该数据;

    insert into class(cname) values
    ('一班'),
    ('二班');
    insert into student values(1,'alex','female','12345',1);
    update student set sname = 'wupeiqi' where id = 1;
    delete from student where id = 1; 
    

    4、查询student表中,每个班级的学生数;

    insert into student(sname,class_id) values
    ('alice',1),
    ('alex',2); 
    select count(ID) from student;
    

    5、修改credit字段为unique属性;

    alter table student modify credit varchar(32) not null unique;
    

    6、请使用命令在你本地数据库中增加一个用户,并给该用户授予创建表的权限;

    grant create on *.* to 'alice'@'localhost' identified by '123';        
    

    7、请使用pymsql模块连接你本地数据库,并向student表中插入一条数据;

    # -*- coding:utf-8 -*-
    import pymysql
    conn = pymysql.connect(
        host = '127.0.0.1',
        port = 3306,
        user = 'root',
        password = '123',
        db = 'db_bj',
        charset = 'utf8'
    )
    cursor = conn.cursor()
    sql = 'insert into student(sname,credit,class_id) values (%s,%s,%s)'
    rows = cursor.execute(sql,('alcie','1234567',1))
        
    conn.commit()
    cursor.close()
    conn.close()
        
    if rows:
        print('success')
    else:
        print('failed')
    

    8、请使用mysqldump命令备份student表;

    mysqldump -uroot -p123 db_bj student > /home/bj/桌面/myfile/student.sql
    

    9、创建一张名为student_insert_log的表,要求每次插入一条新数据到student表时,都向student_insert_log表中插入一条记录,记录student_id, insert_time;

    mysql> desc student;
    +----------+-----------------------+------+-----+---------+----------------+
    | Field    | Type                  | Null | Key | Default | Extra          |
    +----------+-----------------------+------+-----+---------+----------------+
    | ID       | int(11)               | NO   | PRI | NULL    | auto_increment |
    | sname    | varchar(16)           | NO   |     | NULL    |                |
    | gender   | enum('male','female') | NO   |     | female  |                |
    | credit   | varchar(32)           | NO   | UNI | NULL    |                |
    | class_id | int(11)               | NO   | MUL | NULL    |                |
    +----------+-----------------------+------+-----+---------+----------------+
      
    create table student_insert_log(
        student_id int not null,
        insert_time datetime not null
    );
        
    创建一个触发器:
    delimiter //
    create trigger tri_after_insert_student after insert on student for each row
    begin
        insert into student_insert_log values(new.ID,now());
    end //
    delimiter ;
        
    insert into student(sname,credit,class_id) values ('alice','123',2);
    insert into student(sname,credit,class_id) values 
    ('egon1','1234',1),
    ('egon2','12345',2);
        
    mysql> select * from student;
    +----+-------+--------+---------+----------+
    | ID | sname | gender | credit  | class_id |
    +----+-------+--------+---------+----------+
    |  4 | alcie | female | 123456  |        1 |
    |  7 | alcie | female | 1234567 |        1 |
    |  8 | alice | female | 123     |        2 |
    |  9 | egon1 | female | 1234    |        1 |
    | 10 | egon2 | female | 12345   |        2 |
    +----+-------+--------+---------+----------+
        
    mysql> select * from student_insert_log;
    +------------+---------------------+
    | student_id | insert_time         |
    +------------+---------------------+
    |          8 | 2018-04-24 21:29:46 |
    |          9 | 2018-04-24 21:32:05 |
    |         10 | 2018-04-24 21:32:05 |
    +------------+---------------------+
    
    

    10、创建一张名为student_update_log的表,要求每次更新student表中的记录时,都向student_update_log表中插入一条记录,记录student_id, update_time;

    create table student_update_log(
        student_id int not null,
        update_time datetime
    );  
        
    创建一个触发器
    delimiter //
    create trigger tri_after_update_student after update on student for each row
    begin
        insert into student_update_log values(new.ID,now());
    end //
    delimiter ;
        
    show triggersG;
        
    update student set sname = 'alex' where ID in (9,10);
    mysql> select * from student;
    +----+-------+--------+---------+----------+
    | ID | sname | gender | credit  | class_id |
    +----+-------+--------+---------+----------+
    |  4 | alcie | female | 123456  |        1 |
    |  7 | alcie | female | 1234567 |        1 |
    |  8 | alice | female | 123     |        2 |
    |  9 | alex  | female | 1234    |        1 |
    | 10 | alex  | female | 12345   |        2 |
    +----+-------+--------+---------+----------+
    5 rows in set (0.00 sec)
        
    mysql> select * from student_update_log;
    +------------+---------------------+
    | student_id | update_time         |
    +------------+---------------------+
    |          9 | 2018-04-24 21:47:24 |
    |         10 | 2018-04-24 21:47:24 |
    +------------+---------------------+
    
  • 相关阅读:
    LeetCode "Median of Two Sorted Arrays"
    LeetCode "Distinct Subsequences"
    LeetCode "Permutation Sequence"

    LeetCode "Linked List Cycle II"
    LeetCode "Best Time to Buy and Sell Stock III"
    LeetCode "4Sum"
    LeetCode "3Sum closest"
    LeetCode "3Sum"
    LeetCode "Container With Most Water"
  • 原文地址:https://www.cnblogs.com/alice-bj/p/8942733.html
Copyright © 2011-2022 走看看