Python语言栈面经高频率问题总结(内附答案)

1. python装饰器和应用场景?

python装饰器用于在不改变原函数内部代码的前提下,为函数扩展功能,其形式为@装饰器,入门用法为记录原函数的执行时间,或者打印原函数执行日志,其基础功能实现代码如下:

import time
def logger(func):
    def wrapper(*args, **kw):
        print('我准备开始计算:{} 函数了:'.format(func.__name__))

        # 真正执行的是这行。
        func(*args, **kw)

        print('啊哈,我计算完啦。给自己加个鸡腿!!')

    return wrapper

def timmer(func):
    def timeCalculate(*args,**kwargs):
        print("函数开始执行,开始计时")
        starttime = time.time()

        func(*args,**kwargs)

        endtime = time.time()
        print("函数运行完毕,时间为",(endtime-starttime)*1000)
    return timeCalculate

@timmer
@logger
def add(x, y):
  print('{} + {} = {}'.format(x, y, x+y))

if __name__ == '__main__':
    add(1,2)

通过上述代码,可看出装饰器可以多个叠加,其叠加后效果为包含式,timmer,logger,func函数,执行结果为:

函数开始执行,开始计时
我准备开始计算:add 函数了:
1 + 2 = 3
啊哈,我计算完啦。给自己加个鸡腿!!
函数运行完毕,时间为 0.0

 装饰器作为一个函数而言,上述用法是没有参数的用法,进阶的做法为给装饰器传参,根据执行函数的不同,作出相应的功能。如下面代码所示

def say_hello(contry):
  def wrapper(func):
    def deco(*args, **kwargs):
      if contry == "china":
        print("你好!")
      elif contry == "america":
        print('hello.')
      else:
        return
 
      # 真正执行函数的地方
      func(*args, **kwargs)
    return deco
  return wrapper

结论:

带有参数的装饰器即为在无参数的装饰器的基础上外加一层带参数的函数,为上述的sayhello(country),内部的两层函数与前面类似。可以通过此种方式,传递参数,完成功能拓展。

2. 元组和数组的区别?是否都可以作为字典的键?

这是一个带坑的问题,python的语言基础里面,没有数组arrays,这里的数组不是list列表,实际上是讨论list,turple,arrays三者之间的区别。

2.1列表list

  • 方括号[ ],成员之间以,分割
  • 可变的数据类型,可以进行增删改查,
  • 成员类型不限

2.2元组tuple

  • 小括号(),成员之间以,分割,索引为tup[2]
  • 不可变数据类型,不可更改,可合并两个元组,不可删除元组的成员,

2.3数组arrays

准确来说Python中是没有数组类型的,只有列表(list)和元组(tuple), 数组是numpy库中所定义的,所以在使用数组之前必须下载安装numpy库。

  • array中的类型全部相同,不同元素之间以空格分隔
  • 可以是多维,获取其维度使用np.shape()

字典的key键:

作为字典的key键要求必须是可哈希的对象,列表和字典这样的可变类型,由于他们不可哈希,所以不能作为键,而所有不可变的类型都是可哈希的;

值相等的数字表示相同的键,即整型数字1和浮点数1.0的哈希值是相同的,它们是相同的键。

为什么键必须是可哈希的?

        解释器调用哈希函数,根据字典中键的值来计算存储你的数据的位置。如果键是可变对象,它的值可改变。如果键发生变化,哈希函数会映射到不同的地址来存储数据。如果这样的情况发生,哈希函数就不可能可靠地存储或获取相关的数据。选择可哈希的键的原因就是因为它们的值不能改变。
        数字和字符串可以被用做字典的键,元组是不可变的但也可能不是一成不变的,因此用元组做有效的键必须要加限制:若元组中只包括像数字和字符串这样的不可变参数,才可以作为字典中有效的键。

 以下代码中的元组中包含列表,即为可以改动。

tuple = (1, 2, 3, [1, 4, 7])
print(tuple)
tuple[3][2] = 100
print(tuple)

其内存地址表示为:

 

 上图来源:https://blog.csdn.net/machi1/article/details/86601119https://blog.csdn.net/machi1/article/details/86601119

 结论:

列表list不能作为字典的键,其可变,而作为字典的键必须可哈希,即为通过键所指向的位置找到其映射的数据出的内存地址。

元组可以成为字典的键,但是必须加限制,元组中的元素必须是数字和字符串这样的不可变参数。

3. 数组遍历的循环遍历方法

1. for  in方法

colours = ["red","green","blue"]
for colour in colours:
    print colour
 
# red
# green
# blue

2. 序号:

colours = ["red","green","blue"]
for i in range(0, len(colours)):
    print i, colour[i]
 
# 0 red
# 1 green
# 2 blue

4. python内变量存储方式:

python中一切变量都是对象,存储的不是变量本身,而是其内存地址

4.1存储类型

引用语义:在python中,变量保存的是对象(值)的引用,我们称为引用语义。采用这种方式,变量所需的存储空间大小一致,因为变量只是保存了一个引用。也被称为对象语义和指针语义。

值语义:有些语言采用的不是这种方式,它们把变量的值直接保存在变量的存储区里,这种方式被我们称为值语义,例如C语言,采用这种存储方式,每一个变量在内存中所占的空间就要根据变量实际的大小而定,无法固定下来。

4.2 python与C++的区别

  • 各自支持的内建数据类型不同,此处可以在各自语言的入门课程中轻松查到,不一一列举了。
  • Python是动态类型的语言,而C, C++是静态类型。静态类型的变量需要在编译运行之前就显式声明其类型,而动态类型则不用。
  • 变量与内存地址的关系不同:在C语言中,当编译器为变量分配一个空间时,当变量改变值时,改变的是这块空间中保存的值,在程序运行中,变量的地址就不能再发生改变了;
  • Python不同,它的变量与C语言中的指针相似,当变量赋值时,编译器为数值开辟一块空间,而变量则指向这块空间,当变量改变值时,改变的并不是这块空间中保存的值,而是改变了变量指向的空间,使变量指向另一空间。

5. python的垃圾回收机制 

引用计数法:在Python中每一个对象的核心就是一个结构体PyObject,它的内部有一个引用计数器(ob_refcnt)。程序在运行的过程中会实时的更新ob_refcnt的值,来反映引用当前对象的名称数量。当某对象的引用计数值为0,那么它的内存就会被立即释放掉。

引用计数法有其明显的优点,如高效、实现逻辑简单、具备实时性,一旦一个对象的引用计数归零,内存就直接释放了。将垃圾回收随机分配到运行的阶段,处理回收内存的时间分摊到了平时,正常程序的运行比较平稳。但是,引用计数也存在着一些缺点,通常的缺点有:

  • 逻辑简单,但实现有些麻烦。每个对象需要分配单独的空间来统计引用计数,这无形中加大的空间的负担,并且需要对引用计数进行维护,在维护的时候很容易会出错。
  • 在一些场景下,速度慢。正常来说垃圾回收会比较平稳运行,但是当需要释放一个大的对象时,比如字典,需要对引用的所有对象循环嵌套调用,从而可能会花费比较长的时间。
  • 循环引用。这将是引用计数的致命伤,引用计数对此是无解的,因此必须要使用其它的垃圾回收算法对其进行补充。

 6. python导入模块的方式

主要有以下两种:

  1. import 模块名1 [as 别名1], 模块名2 [as 别名2],…:使用这种语法格式的 import 语句,会导入指定模块中的所有成员(包括变量、函数、类等)。不仅如此,当需要使用模块中的成员时,需用该模块名(或别名)作为前缀,否则 Python 解释器会报错。
  2. from 模块名 import 成员名1 [as 别名1],成员名2 [as 别名2],…: 使用这种语法格式的 import 语句,只会导入模块中指定的成员,而不是全部成员。同时,当程序中使用该成员时,无需附加任何前缀,直接使用成员名(或别名)即可。

7. 多态,重载,重写之间的区别

继承:子类继承父类的相关方法,可以重写父类的方法

重写:在子类继承中,子类可以重写父类的方法。

重载:方法重载是在一个类中,定义了多个方法名相同,但是其参数数量不同或者数量相同类型和次序不同的方法。

多态:方法重载是一个类的多态性的体现,方法重写是子类与父类的另一种多态性体现

8.python2与python3的特性与区别

  1. python2支持unicode和str字符,python3支持的unicode的字符
  2. python2采用相对路径import,更容易出错,python3采用绝对路径import
  3. python2允许tab式缩进与space式缩进共存,而python3不允许
  4. 部分函数废弃类
    1. xrange
    2. print
    3. long整型
    4. file函数->open
    5. <>->!=

9. python中的三元运算子?

三元运算符通常在Python里被称为条件表达式,这些表达式基于真(true)/假(false)的条件判断,if else语句的简化版本。元组的01取值

is_fat = True
state = "fat" if is_fat else "not fat"
fat = True
fitness = ("skinny", "fat")[fat]
print("Ali is", fitness)
#输出: Ali is fat

10 python中match和search的区别是什么?

match需要从第一个开始匹配,第一个不符合,则返回None

search是扫描全局,更为准确,速度慢

import re
str = "谢谢您,关注公众号:程序IT圈";

result = re.match('程序IT圈', str)
print(result); //None

result2 = re.match('谢谢您', str)
print(result2); //<re.Match object; span=(0, 3), match='谢谢您'>

result3 = re.search('程序IT圈', str)
print(result3); //<re.Match object; span=(10, 15), match='程序IT圈'>

result4 = re.search('程序员', str)
print(result4); //None
-----------------------------------
说说Python中search()和match()的区别?
https://blog.51cto.com/u_13294304/3356131

11. python中的pass,闭包是什么?

pass不做任何操作,占位语句。

闭包的要求如下:

#闭包函数的实例
# outer是外部函数 a和b都是外函数的临时变量
def outer( a ):
    b = 10
    # inner是内函数
    def inner():
        #在内函数中 用到了外函数的临时变量
        print(a+b)
    # 外函数的返回值是内函数的引用
    return inner

12. python中如何实现单例模式?

单例模式:单个实例,在全局中仅有一个实例,在调用该类时,如果没有实例,则会创建,如果有实例,则会调用已经存在的实例。

优点:仅有一个实例,节省空间;避免数据同步的问题。

创建方式:通过函数装饰器,类装饰器,

def singleton(cls):

    # 创建一个字典用来保存被装饰类的实例对象 _instance = {}
    def _singleton(*args, **kwargs):
        # 判断这个类有没有创建过对象,没有新创建一个,有则返回之前创建的  
        if cls not in _instance:
            _instance[cls] = cls(*args, **kwargs)
        return _instance[cls]
    return _singleton

@singleton
class A(object):
    def __init__(self, a=0):
        self.a = a


a1 = A(1)
a2 = A(2)
# id()函数可以获取对象的内存地址,同一内存地址即为同一对象
print(id(a1), id(a2))

类的方式:

class Singleton(object):
    def __init__(self, *args, **kwargs):
        pass
        
    @classmethod
    def get_instance(cls, *args, **kwargs):
        # hasattr() 函数用于判断对象是否包含对应的属性 , 这里是看看这个类有没有 _instance 属性  
        if not hasattr(Singleton, '_instance' ):
            Singleton._instance = Singleton(*args, **kwargs)

        return Singleton._instance


s1 = Singleton()  # 使用这种方式创建实例的时候 , 并不能保证单例 
s2 = Singleton.get_instance()  # 只有使用这种方式创建的时候才可以实现单例 
s3 = Singleton()
s4 = Singleton.get_instance()

print(id(s1), id(s2), id(s3), id(s4))

其实这种方式的思路就是,调用类的get_instance()方法去创建对象,get_instance方法会判断之前有没有创建过对象,有的话也是会返回之前已经创建的对象,不再新创建,但是这样有一个弊端,就是在使用类创建s3 = Singleton()这种方式的时候,就不能保证单例了,也就是说在创建类的时候一定要用类里面规定的get_instance()方法创建。 再者,当使用多线程时这样也会存在问题,我们来看下面的代码

Python实现单例模式的几种方式 - 掘金单例模式是一种常用的软件设计模式,在应用这个模式时,类只会生成一个实例对象。下面就来介绍一下Python实现单例模式的几种方式。https://juejin.cn/post/6993944421133713415

13. python中变量的作用域

LEGB:

L:locol局部作用域,包含在def关键字定义的语句块中,即在函数中定义的变量

E :enclosing;闭包

G:global 全局作用域,模块层次中定义的变量,每一个模块都是一个全局作用域。也就是说,在模块文件顶层声明的变量具有全局作用域,从外部开来,模块的全局变量就是一个模块对象的属性

B:built-in 系统内固定模块里定义的变量,即系统自带的的变量函数之类的。如预定义在builtin 模块内的变量。

python变量的作用域:局部变量和全局变量_涤生大数据的博客-CSDN博客_python局部变量的作用域变量定义以后,是有一定的使用范围,称之为变量的作用域。比如Java中,变量的使用范围是变量声明时所在的{}范围,而python的作用域对应的为同一个缩进。按着变量的使用范围给变量划分成如下两种: 1.全局变量:就是在函数外部定义的变量 2.局部变量:就是在函数内部定义的变量1.全局函数和局部函数的作用域 局部变量作用域:只能在...https://blog.csdn.net/qq_26442553/article/details/81566571

14.python的内存管理与垃圾回收机制?如何避免内存泄露?

Python 内存管理和垃圾回收机制 - 简书内存管理机制 可变对象/不可变对象 可变对象:如列表、字典等,本质是不论怎么改变值,他的地址都不会发生改变。 不可变对象:如int、float、bool、str、元组等,当你...https://www.jianshu.com/p/892aa84e5814

内存管理机制:

  1. 可变/不可变数据:可变对象存储其内存地址,不随值的改变而改变,不可变数据存储的值,其值改变后,内存也会发生变化。
  2. 内存分配原则:一切皆对象,在底层中每个对象都是基于结构体实现的,而这些结构体里都有着上下指向、引用计数和数据类型的属性,在初始化时都会给数据的引用计数设置为1。
  3. 缓存机制:int,、float、str、list。将某个数据删除时,其可能不会将这个对象完全销毁,而是将对象存放到一个链表中,当又创建同类型的对象时,将会直接赋值给缓存的同类型对象,再通过变量引用指向他,举例:
>>> a = "xxx"
>>> id(a)
2436976108520
>>> del a
# 删除了字符串a
>>> b = "yyy"
# 创建字符串b
>>> id(b)
# 可以发现内存地址时一样的
2436976108520

 垃圾回收机制:

查看引用次数

可以用sys.getrefcount()方法来查看引用次数,要注意因为将内容传入该方法时引用也会加1,所以我们实际想知道的引用次数应该是输出的结果减一,举例:

>>> a = 1000
>>> sys.getrefcount(a)
# 加上传入方法的a,引用次数为2
2
>>> b = a
>>> sys.getrefcount(a)
# 因为b也引用了a,所以引用次数加1
3
>>> del b
>>> sys.getrefcount(a)
# 删除了b以后引用次数减1
2

 当引用计数出现在循环引用计数中时:Python提供了gc.collect()方法用于手动回收数据,举例:

import gc

class L(list):
    def __del__(self):
        print(self, "end")

a = L([1,2,3])
b = L([1,2,a])
a[-1] = b
del a, b
# 手动回收第0代(后面会介绍分代回收,总共有3代)
print("gc generation 0 nums:", gc.collect(0))
print("end")

# [1, 2, [1, 2, [...]]] end
# [1, 2, [1, 2, [...]]] end
# gc generation 0 nums: 2
# end

避免内存泄露? 

出现原因:

1. 所用到的用 C 语言开发的底层模块中出现了内存泄露。

2. 代码中用到了全局的 list、 dict 或其它容器,不停的往这些容器中插入对象,而忘记了在使用完之后进行删除回收

3. 代码中有“引用循环”,并且被循环引用的对象定义了__del__方法,就会发生内存泄露。

如何避免:

  1. 全局的容器list,dict等尤其注意删除回收
  2. 对于循环引用的内容,使用gc.collect()方法用于手动回收数

15. yield函数的用法?

应用场景:

当需要建立一个很大的list,而list大到内存不够用,一次循环的结果就会将内存挤爆,此时需要分段循环

循环费时,在处理时加入yield起到延迟的作用,可以先计算好单个循环的结果,节省整个循环调用的时间。简单的例子如下:

def foo():
    print("starting...")
    while True:
        res = yield 4
        print("res:",res)
g = foo()
print(next(g))
print("*"*20)
print(next(g))

#输出为:
starting...
4
********************
res: None
4

类似于return,而“产生一个序列”是因为我们的函数并没有像通常意义那样返回。return隐含的意思是函数正将执行代码的控制权返回给函数被调用的地方。而”yield”的隐含意思是控制权的转移是临时和自愿的,我们的函数将来还会收回控制权。 

python中yield的用法详解——最简单,最清晰的解释_冯爽朗的博客-CSDN博客_python yield首先我要吐槽一下,看程序的过程中遇见了yield这个关键字,然后百度的时候,发现没有一个能简单的让我懂的,讲起来真TM的都是头头是道,什么参数,什么传递的,还口口声声说自己的教程是最简单的,最浅显易懂的,我就想问没有有考虑过读者的感受。接下来是正题:首先,如果你还没有对yield有个初步分认识,那么你先把yield看做“return”,这个是直观的,它首先是个return,普通的retur...https://blog.csdn.net/mieleizhi0522/article/details/82142856

16. 你觉得python2的项目如果迁移到python3,困难会在哪里?

1、Python3去除print语句,加入print()函数实现相同的功能。

2、Python2 中/的结果是整型,Python3 中是浮点类型。

3、字符串存储的区别。python2中 字符串以 8-bit 字符串存储,python3中字符串以 16-bit Unicode 字符串存储。存储格式得到了升级。

4、取值范围的区别。python2中用xrange ,python3中用range。如:python2中的 xrange( 0, 4 ) 改为python3中的range(0,4)。

5、键盘输入的区别。从键盘录入一个字符串,python2中是 raw_input( "hello world" ),python3则是 input( "hello world" )。

6、Python2 中声明元类:_metaclass_ = MetaClass,然而在Python3 中声明元类:

17. python中用过其他的模块?其他模块对于原数据类型的补充有哪些?

requests:常用的 http 模块,常用于发送 http 请求

scrapy:在网络爬虫领域必不可少

pygame:常用于 2D 游戏的开发

numpy:为 Python 提供了很多高级的数学方法

Flask:轻量级开源 Web 开发框架,灵活、开发周期短

Django:一站式开源 Web 开发框架,遵循 MVC 设计

numpy中的array,pandas模块中的dataframe

18. python的自省方法有哪些?

Python实现自省有很多方法,常用的有 

  • type(),判断对象类型

  • dir(), 带参数时获得该对象的所有属性和方法;不带参数时,返回当前范围内的变量、方法和定义的类型列表

  • help() ,  用于查看函数或模块用途的详细说明

  • isinstance(),判断对象是否是已知类型

  • issubclass(),判断一个类是不是另一个类的子类

  • hasattr(),判断对象是否包含对应属性

  • getattr(),获取对象属性

  • setattr(), 设置对象属性

  • id(): 用于获取对象的内存地址

  • callable():判断对象是否可以被调用。

19. 多线程与多进程直接的区别?

1、多线程可以共享全局变量,多进程不能

2、多线程中,所有子线程的进程号相同;多进程中,不同的子进程进程号不同

3、线程共享内存空间;进程的内存是独立的

4、同一个进程的线程之间可以直接交流;两个进程想通信,必须通过一个中间代理来实现

5、创建新线程很简单;创建新进程需要对其父进程进行一次克隆

6、一个线程可以控制和操作同一进程里的其他线程;但是进程只能操作子进程

两者最大的不同在于:在多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响;而多线程中,所有变量都由所有线程共享 。

20. 字典的相关操作?

说说Python字典以及基本操作?每天一道面试好题,轻松拿offer !https://mp.weixin.qq.com/s?__biz=MzA5NzgzODI5NA==&mid=2454037419&idx=4&sn=24d89e3aab0c5d893757ba481d81fa0b&chksm=872bb612b05c3f043956e3be3df5cef2d6c7ebd242e08f7f5a161b3feaee3c30798ed2421cfa&scene=21#wechat_redirect

21. python深浅拷贝?

 

1、浅拷贝

使用copy模块里面的copy方法实现。

改变原始对象中为可变类型的元素的值,会同时影响拷贝对象;

改变原始对象中为不可变类型的元素的值,不会响拷贝对象。

2、深拷贝

copy模块里面的deepcopy方法实现。

深拷贝,除了顶层拷贝,还对子元素也进行了拷贝。

原始对象和拷贝对象所有的可变元素地址都不一样了。

Python深浅拷贝总结:

1,深浅拷贝都是对源对象的复制,占用不同的内存空间。

2,不可变类型的对象,对于深浅拷贝毫无影响,最终的地址值和值都是相等的。

3,可变类型的对象:值相等,地址不相等 。

22. python中迭代器与生成器的区别?

Python中生成器能做到迭代器能做的所有事,而且因为自动创建了__iter__()和next()方法,生成器显得特别简洁,而且生成器也是高效的,使用生成器表达式取代列表解析,同时节省内存。除了创建和保持程序状态的自动生成,当发生器终结时,还会自动跑出StopIterration异常。

1、迭代器

迭代器对象要求支持迭代器协议的对象。在Python中,支持迭代器协议就是实现对象的__iter__()和next()方法。其中__iter__()方法返回迭代器对象本身;next()方法返回容器的下一个元素,在结尾时引发StopIteration异常。

2、生成器

生成器(generator)就是一个函数,它提供了一种实现迭代器协议的便捷方式。生成器与普通函数的区别在于它包含 yield 表达式,并且不需要定义 __iter__()和__next__()。

生成器是一种惰性的序列,如果我们需要创建一个 0~1000000000 的序列,这样大的序列创建出来会占用比较多的内存,生成器就是为了解决这样的问题 。


redis保存的数据类型?以及缓存淘汰的策略是什么?

Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。

16.MyISAM和InnoDB有什么区别?python项目开发中sql注入是怎么产生的,如何防止?

17. 解释一下线程池的工作原理。

18. 什么是数据库连接池,他有什么样都作用,平时项目都是用来哪些组件来实现项目中都连吃

如何进行数据库优化?

 排序算法

冒泡排序:每两个数之间对比,将a<b,则不动,反之则交换,

选择排序:找出最小的一个元素,和第一个元素交换,

插入排序:类似于打牌,每轮将一个元素置于已经有序的序列中的合适位置。遍历原数组与有序数组两个,因此为O(n2)

O(nlogn)

快速排序:取出一个作为基数,左右指针分别移动,将比基数大的放右边,比基数小的放在左边。

归并排序:递归拆分为两个有序数列,然后通过对比的方式将分支有序数列重新组合为整体的有序数列。

时间复杂度O(n2)

冒泡排序

代码模板(JAVA)

public static void bubbleSort(int[] arr) {
    for (int i = 0; i < arr.length - 1; i++) {
        for (int j = 0; j < arr.length - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                // 如果左边的数大于右边的数,则交换,保证右边的数字最大
                arr[j + 1] = arr[j + 1] + arr[j];
                arr[j] = arr[j + 1] - arr[j];
                arr[j + 1] = arr[j + 1] - arr[j];
            }
        }
    }
}

选择排序

代码模板(JAVA)

public static void selectionSort(int[] arr) {
    int minIndex;
    for (int i = 0; i < arr.length - 1; i++) {
        minIndex = i;
        for (int j = i + 1; j < arr.length; j++) {
            if (arr[minIndex] > arr[j]) {
                // 记录最小值的下标
                minIndex = j;
            }
        }
        // 将最小元素交换至首位
        int temp = arr[i];
        arr[i] = arr[minIndex];
        arr[minIndex] = temp;
    }
}

插入排序

  • 类似打牌

代码模板(JAVA)

public static void insertSort(int[] arr) {
    // 从第二个数开始,往前插入数字
    for (int i = 1; i < arr.length; i++) {
        int currentNumber = arr[i];
        int j = i - 1;
        // 寻找插入位置的过程中,不断地将比 currentNumber 大的数字向后挪
        while (j >= 0 && currentNumber < arr[j]) {
            arr[j J+ 1] = arr[j];
            j--;
        }
        // 两种情况会跳出循环:1. 遇到一个小于或等于 currentNumber 的数字,跳出循环,currentNumber 就坐到它后面。
        // 2. 已经走到数列头部,仍然没有遇到小于或等于 currentNumber 的数字,也会跳出循环,此时 j 等于 -1,currentNumber 就坐到数列头部。
        arr[j + 1] = currentNumber;
    }
}

时间复杂度O(nlogn)

快速排序

  1. 从数组中取出一个数,称之为基数(pivot)

  2. 左右双指针分别向中间缩进,将比基数大的数字放到它的右边,比基数小的数字放到它的左边,不符合的时候指针变量交替,同时切换运行指针,直到两指针指向同一位置。遍历完成后,数组被分成了左右两个区域

  3. 将左右两个区域视为两个数组,重复前两个步骤,直到排序完成

代码模板(C++)

void QuickSort(int array[], int start, int last)
{
    int i = start;
    int j = last;
    int temp = array[i];
    if (i < j)
    {
        while (i < j)
        {
            while (i < j &&  array[j]>=temp ) j--;
            if (i < j)
            {
                array[i] = array[j];
                i++;
            }
            while (i < j && temp > array[i]) i++;
            if (i < j)
            {
                array[j] = array[i];
                j--;
            }
        }
        //把基准数放到i位置
        array[i] = temp;
        //递归方法
        QuickSort(array, start, i - 1);
        QuickSort(array, i + 1, last);
    }
}

堆排序

堆:符合以下两个条件之一的完全二叉树:

  • 根节点的值 ≥ 子节点的值,这样的堆被称之为最大堆,或大顶堆;

  • 根节点的值 ≤ 子节点的值,这样的堆被称之为最小堆,或小顶堆。

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

堆排序过程如下:

  • 用数列构建出一个大顶堆,取出堆顶的数字;

  • 调整剩余的数字,构建出新的大顶堆,再次取出堆顶的数字;

  • 循环往复,完成整个排序。

构建大顶堆有两种方式:

  • 方案一:从 0 开始,将每个数字依次插入堆中,一边插入,一边调整堆的结构,使其满足大顶堆的要求;

  • 方案二(较常用):将整个数列的初始状态视作一棵完全二叉树,自底向上调整树的结构,使其满足大顶堆的要求。

完全二叉树有如下性质:

  • 对于完全二叉树中的第 i 个数,它的左子节点下标:left = 2i + 1

  • 对于完全二叉树中的第 i 个数,它的右子节点下标:right = left + 1

  • 对于有 n 个元素的完全二叉树(n >2),它的最后一个非叶子结点的下标:n/2 - 1

方案二堆排序思想:

调整数组结构使其所表示的完全二叉树为大顶堆,然后将最大值放在末尾(即交换数组头和数组末尾的元素),然后对剩余数组重复上述步骤,即可完成堆排序。

方案二动图演示如下

public static void heapSort(int[] arr) {
    // 构建初始大顶堆
    buildMaxHeap(arr);
    for (int i = arr.length - 1; i > 0; i--) {
        // 将最大值放到数组最后
        exchange(arr, 0, i);
        // 调整剩余数组,使其满足大顶堆
        maxHeapify(arr, 0, i);
    }
}

// 构建初始大顶堆
public static void buildMaxHeap(int[] arr) {
    // 从最后一个非叶子结点开始调整大顶堆,最后一个非叶子结点的下标就是 arr.length / 2-1
    for (int i = arr.length / 2 - 1; i >= 0; i--) {
        maxHeapify(arr, i, arr.length);
    }
}

// 调整大顶堆,第三个参数表示剩余未排序的数字的数量,也就是剩余堆的大小
private static void maxHeapify(int[] arr, int i, int heapSize) {
    // 左子结点下标
    int l = 2 * i + 1;
    // 右子结点下标
    int r = l + 1;
    // 记录根结点、左子树结点、右子树结点三者中的最大值下标
    int largest = i;
    // 与左子树结点比较
    if (l < heapSize && arr[l] > arr[largest]) {
        largest = l;
    }
    // 与右子树结点比较
    if (r < heapSize && arr[r] > arr[largest]) {
        largest = r;
    }
    if (largest != i) {
        // 将最大值交换为根结点
        exchange(arr, i, largest);
        // 再次调整交换数字后的大顶堆
        maxHeapify(arr, largest, heapSize);
    }
}
// 交换元素
private static void exchange(int[] arr, int i, int j) {
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

归并排序

  • 归并排序的思想就是递归地将两个有序序列进行合并

  • 将数组按拆分成左右两部分,可以认为左右子序列均为有序序列,对两序列进行合并。

  • 拆分时对子序列递归上述步骤,则子序列优先先完成归并排序成为有序。

代码模板(JAVA)

public static void mergeSort(int[] arr) {
    if (arr.length == 0) return;
    int[] result = mergeSort(arr, 0, arr.length - 1);
    // 将结果拷贝到 arr 数组中
    for (int i = 0; i < result.length; i++) {
        arr[i] = result[i];
    }
}

// 对 arr 的 [start, end] 区间归并排序
private static int[] mergeSort(int[] arr, int start, int end) {
    // 只剩下一个数字,停止拆分,返回单个数字组成的数组
    if (start == end) return new int[]{arr[start]};
    int middle = (start + end) / 2;
    // 拆分左边区域
    int[] left = mergeSort(arr, start, middle);
    // 拆分右边区域
    int[] right = mergeSort(arr, middle + 1, end);
    // 合并左右区域
    return merge(left, right);
}

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值