【程序员笔试面试必会——排序③】高频笔试题、知识点
一、小范围排序
题目:
已知一个几乎有序的数组,几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离要小于k,并且k相对于数组来说比较小。请选择一个合适的排序算法针对这个数据进行排序。
给定一个int数组A,同时给定A的大小n和题意中的k,请返回排序后的数组。
[2, 1, 4, 3, 6, 5, 8, 7, 10, 9], 10, 2
返回:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
思路:
首先看一下时间复杂度O(N)的算法,如:计数排序、基数排序,但因为我们不知道数组的范围,所以这些算法就不去考虑了。
然后看一下时间复杂度O(N^2)的算法,如:冒泡排序、选择排序,这两个排序算法是无论要排序的序列是什么顺序,时间复杂度都是严格的O(N^2)。插入排序,这里可以做到很好的程度,因为插入排序的过程与原始顺序有关,每个移动距离不超过k,对本题来说,插入排序的时间复杂度是不会高于O(N*k)。
最后看一下时间复杂度O(N*logN)的算法,如:快速排序,快速排序与原始数据顺序也是无关的,快速排序是随机选一个数,以这个数对整个数组进行划分,划分出来的两个部分再分别进行递归。归并排序,也是与原始数据顺序无关的,归并排序是把所有的组都打散,然后小组合大组,大组再合更大的组,最后使整个数组有序。
然而这道题的答案是,改进后的堆排序。整个数组的最小值肯定是在 位置0~位置k-1 这个范围里的,首先将 位置0~位置k-1 这k个数组成一个小根堆,这个小根堆的堆顶肯定是整个数组的最小值,把堆顶弹出放在数组的 位置0 上,再将原序列 位置k 上的数放在小根堆的堆顶,对小根堆进行调整,数组第二小的数会作为小根堆的堆顶出现,再把堆顶放在数组的 位置1 上,反复操作,如此弹出堆顶的顺序就是数组排好序的顺序。因为每次调整顺序都是在大小为k的小根堆里调整的,所以每次调整的代价是O(k),总共N个数,所以整个的时间复杂度是O(N*logk)。
Python代码:
# -*- coding:utf-8 -*-
class ScaleSort:
def sortElement(self, A, len_A, k):
i = 0
min_heap = A[0: k]
for n in A[k:]:
# 修复小根堆
for j in xrange(k / 2 + 1, -1, -1):
self.min_heap_fix(min_heap, j, k)
A[i], min_heap[0] = min_heap[0], n
i += 1
for _ in xrange(k):
for j in xrange(len(min_heap) + 1, -1, -1):
self.min_heap_fix(min_heap, j, len(min_heap))
A[i] = min_heap.pop(0)
i += 1
return A
def min_heap_fix(self, A, i, n):
"""
:param A: 小根堆(一维数组)
:param i: 预修复的子树根节点
:param n: 小根堆总的元素数量
"""
j = i * 2 + 1 # i的左子节点下标
# 当i的左子节点存在时
while j < n:
# 当i的右子节点存在,且大于i的左子节点
if j + 1 < n and A[j+1] < A[j]:
j += 1
# 当i的左右子节点都大于i时,修复小根堆结束
if A[j] >= A[i]:
break
# 当i的子节点小于i时,交换节点
A[i], A[j] = A[j], A[i]
i = j # 将i移向于i交换的节点
j = i * 2 + 1 # i的左子节点下标
l = [2, 1, 4, 3, 6, 5, 8, 7, 10, 9]
print ScaleSort().sortElement(l, len(l), 6)