一、基本概念
-
算法
1】概念
解决问题的思想和方法。
2】五大特性
输入:0个或者多个
输出:1个或者多个
有穷性:有限的步骤和可接受的时间
确切性:执行的每一步都有确切的含义,不会产生歧义
可行性:执行的每一步都是可行的。 -
时间复杂度
1】基本概念
就是执行某段程序需要的总步数,然而根据时间复杂度的趋势大致相同,我们采取“大O表示法”,也就是去除这些步骤的旁枝末节,得到大致的表达式,不论内外数字的变化,都能够用同一个表达式代替。
2】实例
import time
start_time = time.time()
# 注意是三重循环
for a in range(0, 1001):
for b in range(0, 1001):
for c in range(0, 1001):
if a**2 + b**2 == c**2 and a+b+c == 1000:
print("a, b, c: %d, %d, %d" % (a, b, c))
end_time = time.time()
print("elapsed: %f" % (end_time - start_time))
print("complete!")
时间复杂度是T(n)=n^3
对其进行改进之后的程序:
import time
start_time = time.time()
# 注意是两重循环
for a in range(0, 1001):
for b in range(0, 1001-a):
c = 1000 - a - b
if a**2 + b**2 == c**2:
print("a, b, c: %d, %d, %d" % (a, b, c))
end_time = time.time()
print("elapsed: %f" % (end_time - start_time))
print("complete!")
时间复杂度是:T(n)=n^2
3】时间复杂度的基本类型
最优时间复杂度
最坏时间复杂度
平均时间复杂度
注意:我们经常所说的时间复杂度指的是最坏时间复杂度,因为它是一种保证。
4】时间复杂度的几条基本计算准则
基本操作:常数项计算,也就是用1来表示
顺序结构:加法计算
循环结构:乘法计算
分支结构:取时间复杂度的最大值
5】常见的时间复杂度
O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)
-
数据结构
数据结构表示一组数据保存的方式,在python中基本数据类型已经不是一种简简单单的数据类型了。其实数据结构就是对基本数据类型的一次封装。 -
程序
我们学的程序就是算法加上数据结构 -
抽象数据类型
就是预先定义出来的数据存储的类型或者结构。 -
timeit模块
1】基本概念
timeit模块可以用来测试一小段Python代码的执行速度。
class timeit.Timer(stmt=‘pass’, setup=‘pass’, timer=)
Timer是测量小段代码执行速度的类。
stmt参数是要测试的代码语句(statment);
setup参数是运行代码时需要的设置;
timer参数是一个定时器函数,与平台有关。
timeit.Timer.timeit(number=1000000)
Timer类中测试语句执行速度的对象方法。number参数是测试代码时的测试次数,默认为1000000次。方法返回执行代码的平均耗时,一个float类型的秒数。
2】实例测试
def test1():
l = []
for i in range(1000):
l = l + [i]
def test2():
l = []
for i in range(1000):
l.append(i)
def test3():
l = [i for i in range(1000)]
def test4():
l = list(range(1000))
from timeit import Timer
t1 = Timer("test1()", "from __main__ import test1")
print("concat ",t1.timeit(number=1000), "seconds")
t2 = Timer("test2()", "from __main__ import test2")
print("append ",t2.timeit(number=1000), "seconds")
t3 = Timer("test3()", "from __main__ import test3")
print("comprehension ",t3.timeit(number=1000), "seconds")
t4 = Timer("test4()", "from __main__ import test4")
print("list range ",t4.timeit(number=1000), "seconds")
3】timeit模块的基本使用
import timeit
li=[]
def t1():
for i in range(10000):
li.append(i)
t = timeit.Timer("t1()","from __main__ import t1")
print(t.timeit(1000))
二、顺序表
-
内存、类型本质和连续存储
数据类型的本质是它可以决定你在计算机中占用多少个内存单元,所有的高级数据类型都是由最基本的数据类型来决定的,它决定了你在内存中如何存储。
1】顺序表的基本形式
元素的下标是逻辑地址,元素存储的物理地址可以通过起始地址加上存储的逻辑地址和存储单元的乘积得到。即:Li=L0+C*i -
顺序表元素的添加
保序添加:T(n)=O(1),最优时间复杂度
不保序添加:T(n)=O(1),时间复杂度
保序添加:T(n)=O(n),最坏时间复杂度
注意:删除也是同一道理 -
基本顺序表与元素外围顺序表
1】为什么下标是从零开始?
0代表的是偏移量,第一个元素下标为零表示不进行偏移。
2】在列表中也许我们存储数据的地址可能是不连续,但是我们存储数据引用的地址是连续的,在列表中,因为可以存储不同数据类型的值,这样的话每一个数据被分配的内存大小是不一样的,为了解决这个问题,我们提出使用列表存储这些数据的引用的地址,然后将这些数据实际的地址位置存储在可能的不同的位置。 -
顺序表的结构
1】表头信息:容量和元素个数
容量表示声明的时候顺序表总共可以存储数据的多少
元素个数表示顺序表当前存储数据的的多少
2】两种实现方式
第一种就是将表头和数据区放在一块,被称为一体式结构。因为容量和元素个数的大小空间我们是知道的,它是两个整数的大小。
第二种方式是将表头和数据区分开被称为分离式结构。表头存储数据的时候需要三个存储单元,分别是存储容量的数据单元、存储元素个数的数据单元和存储数据区引用的地址单元。
3】两种方式的优略问题
我们在使用一体式进行存储的时候,如果说原先声明的存储空间已经不能满足我们存储数据的需求了,那么我们就需要进行扩充,在扩充的时候我们不能在原先的结构上进行修改,因此只能向操作系统索要新的内存空间,这时候就需要我们将这个顺序表上所有的数据重新复制到新的内存空间中,然后释放原先的内存空间,而且内存地址的引用也将会改变。然而如果我们使用分离式结构的话,我们的表头信息是不需要改变,只需要将数据区的信息进行复制和移动。
4】扩充问题
当我们原先申请的空间满了的时候就需要我们对其进行扩充,然而扩充也是需要策略的。一次扩充的太少,效率就会太低,每次扩充都要进行移动和空间的释放。如果一次扩充的太多的话,就会浪费资源。
第一种扩充方式是采用固定数目的;
第二种扩充方式是采用加倍方式的。
三、单链表
- 线性表
1】概念
包括连续存储的顺序表和非连续存储的链表
2】python中变量标识的本质
a=10
b=20
a,b=b,a
a和b表示的是一块存储单元,我们将10和20的数据地址存储在a和b这两个存储空间中了。
-
顺序表和链表的区别
链表的优点是对离散的内存空间达到充分的利用;缺点就是利用的同时额外的开销也是大的。
顺序表的优点是可以一次性定位;缺点是对于顺序表的存储空间必须是连续的,如果一旦改变,其他位置存储的数据都要改变
对于链表来说,花销的时间全部花费在遍历上面了;对于顺序表来说,所有的时间花销在数据的搬迁上面了。 -
单链表
1】结点实现
在结点中一个存放数据,一个存放引用
class SingleNode(object):
def __init__(self,elem):
self.elem=elem
self.next=None
2】单链表的初始化
初始化一个头结点,用来记录链表的头地址。
def __init__(self): # 初始化一个头结点
self.__head=None
3】判断链表是不是空
直接判断头结点是不是空就可以了,因为如果头结点是空的话,后面一定是空的。
def is_empty(self):
return self.__head==None
4】统计链表的长度
def length(self):
cur=self.__head
count=0
while cur!=None:
count+=1
cur=cur.next
return count
5】遍历链表
def travel(self):
cur=self.__head
while cur!=None:
print(cur.elem,end=" ")
cur=cur.next
6】在头部添加数据
def add(self,item):
node=SingleNode(item)
node.next=self.__head # 将头结点作为添加结点的下一个结点
self.__head=node # 将新添加的结点作为头结点
7】在尾部添加数据
def append(self,item):
node=SingleNode(item)
if self.is_empty():
self.__head=node
else:
cur=self.__head # 将头结点放置在当前结点中
while cur.next!=None:
cur=cur.next
cur.next=node
8】在任意位置插入数据
def insert(self,pos,item):
if pos<=0:
self.add(item)
elif pos>=self.length():
self.append(item)
else:
node=SingleNode(item)
count=0
pre=self.__head
while count<(pos-1):
count+=1
pre=pre.next
node.next=pre.next
pre.next=node
9】查找数据是否存在
def search(self,item):
cur=self.__head
while cur!=None:
if cur.elem==item:
return True
cur=cur.next
return False
10】删除数据
首先将头结点作为当前结点;其次肯定要将链表查找一遍查看有没与想要删除的数据,直到遍历完整个链表结束,如果说找到了那么就执行删除并且跳出循环。
在这里声明了两个游标,一个表示前驱结点pre,一个表示当前结点cur;
接下来我们判断当前结点是不是想要删除的结点,如果是就执行删除;如果不是就将当前结点作为前驱结点,然后将当前结点后移一个结点。
删除的时候还需要注意判断该结点是否是第一个结点,因为第一个结点是没有前驱结点;如果是我们就将当前结点的下一个结点直接作为头结点,这样就相当于删除了当前结点;如果说不是第一个结点,说明有前驱结点,那么我们就将当前结点的下一个结点作为前驱结点的下一个结点,这样就删除了当前结点。
def remove(self,item):
cur=self.__head
pre=None # 定义一个直接前驱
while cur!=None: # 遍历所有结点,直到发现所要删除的结点为止
if cur.elem==item: # 如果当前结点就是想要删除的结点
if not pre: # 头指针指向头结点后的一个结点,也就是第一个结点就是想要删除的元素
self.__head=cur.next
else: # 有直接前驱的才能进入
pre.next=cur.next
break
else: # 如果不是就执行后移操作
pre=cur
cur=cur.next
- 单向循环链表
1】设置结点
class Node(object):
def __init__(self,elem):
self.elem=elem
self.next=None
2】单向循环链表的初始化
初始化单向循环链表,如果传入的是None,那么我们将其设置为头结点;如果传入的是一个结点的话,我们不但要将其设置为头结点,还要将该结点的下一个结点指向自己
def __init__(self,node=None):
self.__head=node
if node:
node.next=node
3】判断是否为空
def is_empty(self):
return self.__head==None
4】计算单向循环链表的长度
def length(self):
cur=self.__head
count=1
while cur.next!=self.__head:
count+=1
cur=cur.next
return count
5】遍历
首先将头结点作为当前结点,判断该链表是否为空,如果为空,我们不做任何操作;如果不是空链表,我们循环的条件是不能然当前借点的下一个结点等于头结点,将其打印出来,当前结点后移,如果当前借点的下一个结点等于头结点,那么我们需要将最后一个结点手动打印出来,因为这个时候最后一个结点没有进入循环体中,也没有自己打印,需要手动进行打印。
def travel(self):
cur=self.__head
if self.is_empty():
return
while cur.next!=self.__head:
print(cur.elem,end=" ")
cur=cur.next
# 因为在退出循环的时当前结点指向尾结点,所以尾结点没有被打印出来。
print(cur.next)
print("")
6】头部添加元素
def add(self,item):
node=Node(item) # 首先将这个数字封装成结点
if self.is_empty(): # 判断当前链表是否为空,如果是
self.__head=node # 将这个结点作为头结点
node.next=self.__head # 将这个结点的下一个结点指向它自己
else:
node.next=self.__head # 新结点的下一个结点作为头结点
cur=self.__head # 当前结点也指向头结点
while cur.next!=self.__head: # 当前结点的下一个结点不是头结点的时候一直向后移动
cur=cur.next
cur.next=node # 将这个新结点作为当前结点的下一个结点
self.__head=node # 而且要将新结点作为头结点
7】在链表的末尾插入元素
def append(self,item):
node=Node(item)
if self.is_empty():
self.__head=node
node.next=self.__head
else:
cur=self.__head
while cur.next!=self.__head:
cur=cur.next
node.next=self.__head
cur.next=node
8】在链表的指定位置插入元素
首先判断指定的位置是否符合实际,如果指定的位置小于等于0,我们按照插入在开头位置对待,如果指定的位置超过了链表的长度,那么我们按照添加在链表的结点对待。如果是一般情况时,首先将数据封装成结点,将头结点指向当前位置,初始化计数长度为0,判断,进入循环,向后移动,一旦跳出循环,就将当前结点的下一个位置定义为新结点的的下一个位置,将当前结点定义为当前结点的下一个结点。(先后面再前面)
def insert(self,pos,item):
if pos<=0:
self.add(item)
elif pos>self.length():
self.append(item)
else:
node=Node(item)
cur=self.__head
count=0
while count<(pos-1):
count+=1
cur=cur.next
node.next=cur.next
cur.next=node
- 查找指定元素
def search(self,item):
if self.is_empty(): # 如果当前链表为空,直接返回False
return False
cur=self.__head # 将头结点设置为当前结点
if cur.elem==item: # 判断头结点是否等于所要查找的元素,如果是,直接返回True
return True
while cur.next!=self.__head: # 这个是一般情况,进入循环,直到遍历完第一遍为止,如果遍历完还没有找到元素,那么认为该结点不存在
cur=cur.next # 结点后移一位
if cur.elem==item: #当前结点如果等于查找结点,返回True
return True
return False
10】 删除指定元素
- 双向链表
代码暂无
四、栈和队列
-
基本概念
顺序表和链表描述的是数据如何存储,栈和队列描述的是数据如何操作。
1】栈的特点:先进后出
2】队列特点:先进先出 -
栈的实现
五、排序与搜索
- 冒泡排序
1】算法原理
冒泡排序就是首先中一列数中从头开始,第一个和第二个进行比较,如果说第一个比第二个大(这里按升序排列),则两个数进行交换,否则继续比较第二个和第三个。。。总共比较n-1次,这是内层循环,内层循环主要是为了控制排序从头走到尾,而外层循环则是控制需要这样排几次。
2】算法技巧
外层循环控制走多少次,内层循环控制从头走到尾。
3】代码实现
def BubbleSort(alist):
n=len(alist)
for j in range(0,n-1):
for i in range(0,n-1-j):
if alist[i]>alist[i+1]:
alist[i],alist[i+1]=alist[i+1],alist[i]
return alist
a=[9,3,8,2,5,1]
result=BubbleSort(a)
print(result)
代码优化:
def bubbleSort2(alist):
n=len(alist)
for j in range(0,n-1):
count=0
for i in range(0,n-1-j):
if alist[i]>alist[i+1]:
alist[i],alist[i+1]=alist[i+1],alist[i]
count+=1
if count==0:
return alist
return alist
4】时间复杂度
最坏的情况下时间复杂度是:O(n**2)
改进之后的最优时间复杂度是:O(n)
5】稳定性:稳定(排序前和排序后两个相同的数的位置是不发生变化的)
- 选择排序
1】算法原理
选择排序首先将一列数的第一个元素作为最小值(这里按降序排列),和第二个数进行比较,如果最小值比第二个数大,那么就将第二个数作为最小值,将其下标记录下来,一直比较到末尾,总共比较n-1次;外层循环记录进行的轮数,总共进行n-1轮
2】代码实现
def selectSort(alist):
n=len(alist)
for j in range(0,n-1):
min_index=j
for i in range(j+1,n):
if alist[min_index]>alist[i]:
min_index=i
alist[min_index],alist[j]=alist[j],alist[min_index]
return alist
a=[9,3,8,6,5,2]
print(a)
result=selectSort(a)
print(result)
3】时间复杂度
最优时间复杂度:O(n2)
最坏时间复杂度:O(n2)
4】稳定性:不稳定(在比较的过程中两个相同的数的位置会进行交换)
- 插入排序
1】算法原理
一开始我们把第一个元素认为是有序序列,后面的元素认为是无序序列,拿后面的数据与前面的数据挨个进行比较,比较完成后将其插入其中。这样总共进行n-1轮,每一轮比较i(i=1,2,3,…,n-1)次
2】代码实现
def insertSort(alist):
n=len(alist) # 计算列表的长度
for j in range(1,n): # j表示的是还未进行排序的数据
i=j # i-1表示已经进行排序的数据的下标
while i>0: # 直到比较到已经排序的数据的下标变成小于0的数才能结束循环,也就是必须比较到下标为0的数才行
if alist[i-1]>alist[i]: #这时i表示未排序了的数,i-1表示已经排序了的数,
alist[i],alist[i-1]=alist[i-1],alist[i] # 满足条件就进行交换
i -= 1 # 交换之后将下标向前移动一位
else: # 如果没有进行交换说明i之前的数包括i已经正常排序,直接退出,该一轮完成
break
return alist
a=[9,3,8,2,5,1]
print(a)
result=insertSort(a)
print(result)
3】时间复杂度
最优时间复杂度:O(n)
最坏时间复杂度:O(n**2)
4】稳定性:稳定
5】选择排序和插入排序的区别
选择排序也是将数据分成两部分,一部分有序一部分无序,但是选择排序是将未知位置的值放入到已知位置,而插入排序是将已知位置的数据放入到未知的位置。