作者:禅与计算机程序设计艺术
1.背景介绍
作为一名计算机科学、工程或信息技术工作者,在准备面试的时候,面试官需要考察候选人的基础知识、编程能力、分析问题的能力、解决问题的能力、沟通协调的能力、团队合作精神等方面的综合素质。这些都是我们都曾经历过或者了解过的一些面试题型和方法。但如何才能在如此多的基础知识面前,更加能够全面准确地评估候选人呢?本文将分享一些面试技巧,希望能帮助到大家提升面试的能力、出色完成面试。
2.核心概念与联系
2.1.算法(Algorithm)
算法是指用来解决特定类别问题的一组指令集及其顺序,它对输入数据进行预测并产生输出。算法从一定数量的数据中找出特定的模式,然后利用这些模式处理新的输入数据,得到有效的结果。一般来说,算法有两个目的:1.计算;2.运用计算机高速处理功能提高效率。
2.2.时间复杂度(Time Complexity)
算法运行的时间复杂度描述了该算法执行所消耗的时间量,它反映了其运行速度的增长趋势和随输入规模增长所需的时间。时间复杂度通常以时间的最坏情况来衡量,也称为渐进时间复杂度(Asymptotic Time Complexity)。
2.3.空间复杂度(Space Complexity)
算法运行过程中占用的存储空间大小,它反映了算法所需内存的增长趋势。算法的空间复杂度往往依赖于内存的使用方式以及输入数据的大小。空间复杂度通常以空间的最坏情况来衡量,也称为渐进空间复杂度。
2.4.数组(Array)
数组是由相同类型元素组成的一组有序、按索引顺序排列的元素集合,每个数组都有一个固定长度。可以把数组看做是一块连续的内存空间,其中第一个元素的地址是固定的,每一个后续元素的地址都向右偏移一个元素的位置,即数组的第一个元素之后的每个元素的地址都相差一个单位的距离。
2.5.链表(Linked List)
链表是一种物理存储单元上非连续存储结构,数据元素之间的关系通过链接指针来表示。链表中,最基本的构成单位是结点(Node),每个结点包含一个存放数据元素的值和指向下一个结点地址的指针。由于不必按照先后顺序存储,链表在插入和删除时可更灵活地找到需要访问或修改的节点,所以具有随机读取的优点。
2.6.队列(Queue)
队列(Queue)是一种特殊的线性表数据结构,只允许在表的一端(rear)进行插入操作,而在另一端(front)进行删除操作,也就是说,队列是一种先进先出的结构。新元素被加入队列的 rear 端,并从 front 端移除;先进入队列的元素,再离开队列的元素。
2.7.栈(Stack)
栈(Stack)是一种特殊的线性表数据结构,只能在表的一端(top)进行插入操作(push),而在另一端(bottom)进行删除操作(pop),按照后进先出(Last In First Out, LIFO)的原则进行操作。栈结构可以使得计算机易于实现函数调用,有助于程序的开发和维护。
2.8.哈希表(Hash Table)
哈希表是一种根据关键字值直接访问记录的存储结构,它通过把关键码映射到表中一个位置来访问记录。哈希表属于无序表,由动态数组和链表或平衡二叉树组成。在查找、插入和删除元素时平均时间复杂度为O(1)。
2.9.排序算法
排序算法是对一组数据进行排序的方法,主要用于对元素进行比较并重新排列,得到一个新的有序序列。
- 冒泡排序:
冒泡排序是最简单直观的排序算法,它重复地走访要排序的元素列表,一次比较两个元素,如果他们的顺序错误就把它们交换过来。走访数列的次数过多的话,当设置一个计数器的时候,可以少走几步。但是冒泡排序由于两两之间交换元素导致效率低下,所以目前很少用到。
- 插入排序:
插入排序是一种最简单直观的排序算法,它的思想是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序,因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
- 选择排序:
选择排序是一种简单直观的排序算法,它的工作原理如下:首先在未排序序列中找到最小(大)元素,存放在排序序列的起始位置;然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。重复这一过程,直到所有元素均排序完毕。
- 归并排序:
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有的子序列合并成大的子序列,即变成单个的有序序列。若两个序列相邻,则先比较第一个序列的头部元素,谁小就先取谁,然后再把该元素加到结果序列中去,重复这个过程,直到所有的元素均排序完毕。
- 快速排序:
快速排序是对冒泡排序的一种改进版本。通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
3.核心算法原理和具体操作步骤以及数学模型公式详细讲解
3.1.冒泡排序(Bubble Sort)
冒泡排序的基本思路是两两比较相邻的元素,如果他们的顺序错误就把他们交换过来。最坏情况下的时间复杂度是O(n^2),且不稳定。
3.1.1.步骤
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
3.1.2.动图演示
3.1.3.算法实现
def bubble_sort(arr):
n = len(arr)
# Traverse through all array elements
for i in range(n):
# Last i elements are already sorted
for j in range(0, n - i - 1):
# Swap if the element found is greater than the next element
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
# Example usage:
arr = [64, 34, 25, 12, 22, 11, 90]
bubble_sort(arr)
print("Sorted array is:")
for i in range(len(arr)):
print ("%d" %arr[i])
3.1.4.时间复杂度
冒泡排序的时间复杂度是O(n^2),最坏的情况是每次都要进行n次冒泡。
3.2.选择排序(Selection Sort)
选择排序的基本思路是每一次从待排序的数据元素中选出最小(或最大)的一个元素,然后放到已排序好的序列的末尾。这个过程不断重复直到所有的数据元素已经排序完成。选择排序是稳定的算法,其时间复杂度是O(n^2)。
3.2.1.步骤
- 在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
3.2.2.动图演示
3.2.3.算法实现
def selection_sort(arr):
n = len(arr)
# One by one move boundary of unsorted subarray
for i in range(n):
# Find the minimum element in remaining unsorted array
min_idx = i
for j in range(i+1, n):
if arr[min_idx] > arr[j]:
min_idx = j
# Swap the found minimum element with the first element
arr[i], arr[min_idx] = arr[min_idx], arr[i]
# Example Usage:
arr = [64, 34, 25, 12, 22, 11, 90]
selection_sort(arr)
print("Sorted array is:")
for i in range(len(arr)):
print ("%d" %arr[i])
3.2.4.时间复杂度
选择排序的时间复杂度也是O(n^2),因为它要进行n次循环。
3.3.插入排序(Insertion Sort)
插入排序的基本思路是每一步将一个元素按其大小插入到之前已经排好序的序列中去,直到所有的元素都插入完成。插入排序也是一种稳定的算法。
3.3.1.步骤
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果该元素(已排序)大于新元素,将该元素移到下一位置
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置后
- 重复步骤2~5
3.3.2.动图演示
3.3.3.算法实现
def insertion_sort(arr):
for i in range(1, len(arr)):
key = arr[i]
j = i - 1
while j >= 0 and key < arr[j] :
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key
return arr
# Example Usage:
arr = [64, 34, 25, 12, 22, 11, 90]
insertion_sort(arr)
print("Sorted array is:", end=" ")
for i in range(len(arr)):
print (arr[i], end=" ")
3.3.4.时间复杂度
插入排序的时间复杂度也是O(n^2),因为它要进行n-1次循环。
3.4.希尔排序(Shell Sort)
希尔排序的基本思路就是插入排序,不过不是将数据像插入一样一个一个插入,而是设定一个间隔,分批进行插入。
3.4.1.步骤
- 设置一个gap值,每次将gap缩小为一半,增大序列中的元素移动距离。
- 每gap个元素取一次元素进行插入排序。
3.4.2.动图演示
3.4.3.算法实现
def shellSort(arr):
gap = len(arr)//2
while gap > 0:
for i in range(gap,len(arr)):
temp = arr[i]
j = i
while j >= gap and arr[j-gap] >temp:
arr[j] = arr[j-gap]
j -= gap
arr[j] = temp
gap //= 2
# Example Usage:
arr = [64, 34, 25, 12, 22, 11, 90]
shellSort(arr)
print("Sorted array is")
for i in range(len(arr)):
print ("%d"%arr[i]),
3.4.4.时间复杂度
希尔排序的时间复杂度是O(nlogn).
3.5.归并排序(Merge Sort)
归并排序的基本思路是分而治之,先递归划分子序列,然后合并成完整的序列。归并排序的时间复杂度是O(nlogn)。
3.5.1.步骤
- 分割:将数组分割成多个子序列,直至每个子序列只有一个元素
- 合并:将各个子序列进行合并,生成一个新的数组
3.5.2.动图演示
3.5.3.算法实现
def mergeSort(arr):
if len(arr) > 1:
mid = len(arr) // 2
leftArr = arr[:mid]
rightArr = arr[mid:]
# Recursive call on each half
mergeSort(leftArr)
mergeSort(rightArr)
i = j = k = 0
# Copy data to temp arrays left[] and right[]
while i < len(leftArr) and j < len(rightArr):
if leftArr[i] < rightArr[j]:
arr[k] = leftArr[i]
i += 1
else:
arr[k] = rightArr[j]
j += 1
k += 1
# Checking if any element was left
while i < len(leftArr):
arr[k] = leftArr[i]
i += 1
k += 1
while j < len(rightArr):
arr[k] = rightArr[j]
j += 1
k += 1
# Example Usage:
arr = [64, 34, 25, 12, 22, 11, 90]
mergeSort(arr)
print("Sorted array is:")
for i in range(len(arr)):
print("%d" %arr[i])
3.5.4.时间复杂度
归并排序的时间复杂度是O(nlogn),因为它分成两段,然后递归合并。
3.6.快速排序(Quick sort)
快速排序的基本思路是选一个元素,然后左右两边都递归,然后选取中间的那个数作为基准值,左右两边元素在递归排序。快速排序是一种不稳定的排序算法,时间复杂度是O(nlogn)。
3.6.1.步骤
- 设置两个指针low和high,分别指向数组的第一个元素和最后一个元素,即 low=first=0,high=last=size-1
- 用pivot(基准值)分割数组,所有小于 pivot 的元素都放在左边,大于 pivot 的元素都放在右边。
- 此时 low 和 high 分别指向数组的第一个元素和最后一个元素,此时 low=high=p, p 是数组的正确位置
- 左边部分和右边部分递归排序,直至左右边界重合
3.6.2.动图演示
3.6.3.算法实现
def quickSort(arr):
quickSortHelper(arr,0,len(arr)-1)
def quickSortHelper(arr,first,last):
if first<last:
splitpoint = partition(arr,first,last)
quickSortHelper(arr,first,splitpoint-1)
quickSortHelper(arr,splitpoint+1,last)
def partition(arr,first,last):
pivotvalue = arr[first]
leftmark = first+1
rightmark = last
done = False
while not done:
while leftmark <= rightmark and arr[leftmark] <= pivotvalue:
leftmark = leftmark + 1
while arr[rightmark] >= pivotvalue and rightmark >= leftmark:
rightmark = rightmark -1
if rightmark < leftmark:
done = True
else:
temp = arr[leftmark]
arr[leftmark] = arr[rightmark]
arr[rightmark] = temp
temp = arr[first]
arr[first] = arr[rightmark]
arr[rightmark] = temp
return rightmark
# Example Usage:
arr = [64, 34, 25, 12, 22, 11, 90]
quickSort(arr)
print("Sorted array is:")
for i in range(len(arr)):
print("%d" %arr[i])
3.6.4.时间复杂度
快速排序的时间复杂度是O(nlogn),因为它分成两段,然后递归合并。
3.7.堆排序(Heap Sort)
堆排序的基本思路是构造一个大顶堆或小顶堆,然后排序。堆排序的时间复杂度是O(nlogn)。
3.7.1.步骤
- 创建一个堆,然后将第一个元素与最后一个元素交换,然后将最后一个元素与倒数第第二个元素交换。
- 减少堆的高度,一直到堆只有两个元素
- 将第一个元素与倒数第第二个元素交换
- 重复步骤2、3,直到堆的高度为1,即完成排序。
3.7.2.动图演示
3.7.3.算法实现
import heapq
def heapify(arr, n, i):
largest = i # Initialize largest as root
l = 2 * i + 1 # left = 2*i + 1
r = 2 * i + 2 # right = 2*i + 2
# If left child is larger than root
if l < n and arr[largest] < arr[l]:
largest = l
# If right child is larger than largest so far
if r < n and arr[largest] < arr[r]:
largest = r
# Change root, if needed
if largest!= i:
arr[i],arr[largest] = arr[largest],arr[i] # swap
# Heapify the root.
heapify(arr, n, largest)
def heapSort(arr):
n = len(arr)
# Build a maxheap.
for i in range(n//2 - 1, -1, -1):
heapify(arr, n, i)
# One by one extract elements
for i in range(n-1, 0, -1):
arr[i], arr[0] = arr[0], arr[i] # swap
heapify(arr, i, 0)
# Example Usage:
arr = [64, 34, 25, 12, 22, 11, 90]
heapSort(arr)
print("Sorted array is:")
for i in range(len(arr)):
print("%d" %arr[i])
3.7.4.时间复杂度
堆排序的时间复杂度是O(nlogn),因为它创建堆和删除堆需要的时间。