hash、位运算与算法

360 阅读4分钟

1. 哈希的特点

  • 无限的输入,有限的输出
  • 相同的输入必然产生相同的输出
  • 不同的输入也可能产生相同的输出(哈希碰撞)
  • 输出的结果会均匀分布在输出域中(离散性)

2. 布隆过滤器

用于大量数据的去重,和样本大小无关,存在一定的失误率,但一定不会漏判,只会在此基础上过多判断重复数据。

  1. 根据预估样本的数量n和预期失误率p,计算m(位数组的长度)
  2. 根据m和n,计算哈希函数的个数k
  3. 根据m、n、k的值计算失误率 m,n都是向上取整,所以预期失误率 > 真实失误率

3. 一致性哈希

当输出结果较少,因为hash的离散性可能导致输出结果不会均匀分布在输出域中。或当需要添加或移动数据时,添加机器会导致产生非常大的成本。就需要使用一致性哈希进行分配。比如主从同步,集群搭建等配置肯定会涉及到一致性哈希。

    1. 假如说一个哈希函数比作一个点,且输出域被三个点差不多均匀平分
    2. 需要添加一个点,那么这个新的点会向其所在输出域的点,索要它的分布范围,且会影响它门的离散性,导致很难均匀分布
    1. 如此就需要将每点的输出域分为N多个虚拟节点,共同分布在输出域,分的虚拟节点越多,哈希的离散性越好。
    2. 添加新点时,再把虚拟节点添加上去就不会影响导致哈希的离散性特别差 以上就是一致性哈希的概念。

4. 位运算

位运算0、1都表示二进制的0、1

4.1 按位或运算 |

1|1=1、 0|1=1、 1|0=1、 0|0=0

4.2 按位与运算符 &

1&1=1、 0&1=0、 0&0=0、 1&0=0

def is_odd(n):
    """
    判断奇偶性
    """
    if n&1 == 1:
        print("奇数")
    else:
        print("偶数")

def totle_num(n):
    """
    整数n的二进制中1的个数 
    n&(n-1)这个式子什么作用?把n的二进制数字中最右边的1变为0
    """
    count = 0
    while n:
        count+=1
        n = n&(n-1)
    print(count)

4.3 异或运算 ^

1^1=0、 0^1=1、 1^0=1、 0^0=0

异或的几条性质:

  1. 交换律 a^b = b^a
  2. 结合律 a^b^c = a^(b^c)

如:x^0=x、 x^x=0

def singleNumber(nums):
    """
    [0,1,2,0,1,2,3]
    给定一个非空整数列表,除了某个元素只出现一次以外,
    其余每个元素均出现两次。找出那个只出现了一次的元素。
    """
    ret = 0
    for x  in nums:
        ret = ret^x
    return ret

4.4 10进制与2进制互换

def conver(n):
    """
    10进制转2进制
    """
    if n==0:
        return 0
    temp = []
    while n:
        temp.append(n%2)
        n >>=1
    temp.reverse()
    return "".join([str(x) for x in temp])

def reconver(str1):
    """
    2进制转10进制
    """
    temp = [int(x) for x in list(str1)] 
    temp.reverse()
    sum = 0
    for i in range(0,len(temp)):
        sum += (1 << i) * temp[i]  
    return sum

5. 排序算法

5.1 时间复杂度和空间复杂度

  • 时间复杂度用来和空间复杂度是衡量代码优劣的凭证。
  • 时间复杂度就是代码的运行时间优劣,时间复杂度越低,代码越优良。
  • 空间复杂度就是代码使用除输入、输出之外,额外所占用的内存大小,额外占用内存越小,代码越优良。

5.2 时间复杂度

衡量的原则是:

  • 如果运行时间是常数量级,用常数 O(1)表示;
  • 只保留时间函数中的最高阶项;
  • 如果最高阶项存在,则省去最高阶项前面的系数;

例:T(n) = 0.5n^2 + 0.5n

最高阶项为0.5n^2,省去系数0.5,转化的时间复杂度为:

T(n) = O(n^2)

5.3 空间复杂度

衡量的原则是:

  • 如果只有输入和输出,没有使用额外空间,那么空间复杂度为 O(1);
  • 如果有使用额外空间,根据使用的空间大小计算;

5.4 冒泡排序

冒泡排序的原理是:

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
def buddle_sort(list1):
    """
    冒泡排序
    """
    if list1 is None or len(list1) <= 1:
        return
    for end in range(len(list1) - 1, 0, -1):
        for j in range(0, end):
            if list1[j] > list1[j + 1]:
                swap(list1, j, j + 1)
  • 冒泡排序的时间复杂度: O(n^2)
  • 冒泡排序的时间复杂度: O(1)

5.5 选择排序

选择排序的原理是:

  1. 每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置
  2. 然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到全部待排序的数据元素排完。
def sellection_sort(list1):
    """
    选择排序
    :param list1:
    :return:
    """
    if list1 is None or len(list1) <= 1:
        return

    for i in range(0, len(list1)-1):
        min_index = i

        for j in range(i+1, len(list1)):
            if list1[j] < list1[min_index]:
                min_index = j

        swap(list1, i, min_index)
  • 时间复杂度 O(N^2)
  • 额外空间复杂度 O(1)

5.6 插入排序

如果有一个已经有序的数据序列,要求在这个已经排好的数据序列中插入一个数,但要求插入后此数据序列仍然有序,这个时候就要用到插入排序。

插入排序的原理是: 将一个数据通过比较,插入到已经排好序的有序数据中的前面(或后面)。

def insert_sort(list1):
    """
    插入排序
    """
    for i in range(1, len(list1)):
        temp = list1[i]
        j = i - 1
        while j >= 0 and temp < list1[j]:
            list1[j + 1] = list1[j]
            j -= 1
        list1[j + 1] = temp
  • 时间复杂度 O(N^2)

  • 额外空间复杂度 O(1)

5.7 归并排序

冯诺依曼提出的排序算法 归并排序的原理是:

  1. 先将列表两分为2个子列表,并使2个子列表有序化
  2. 一直两分到每个子列表只有一个元素,再比较大小有序排列,直到全部合并
def merge_sort(list1, left, right):
    """
    归并排序,采用 分治思想,先让左半边和右半边分别有序,再合并
    """
    if left < right:
        mid = (left+right) >> 1
        merge_sort(list1, left, mid)
        merge_sort(list1, mid+1, right)
        merge(list1, left, mid, right)


def merge(list1, left, mid, right):
    """
    两个有序区间合并为一个大区间
    """
    temp = []
    p1 = left
    p2 = mid+1

    while p1 <= mid and p2 <= right:
        if list1[p1] <= list1[p2]:
            temp.append(list1[p1])
            p1 += 1
        else:
            temp.append(list1[p2])
            p2 += 1

    while p1 <= mid:
        temp.append(list1[p1])
        p1 += 1

    while p2 <= right:
        temp.append(list1[p2])
        p2 += 1

    for i in range(0, len(temp)):
        list1[left+i] = temp[i]
  • 如果设定N样本量的时间为T(N),可以得出:T(N) = 2T(N/2)+O(N)

  • 根据主定理,详情请看主定理 Master Theorem,算出时间复杂度O(N*logN)

  • 额外空间复杂度O(N),论文级别的可以做到O(1), 归并排序内部缓存法

相关问题:

  • 两个有序列表求 公共元素
  • 两个有序列表的合并
  • 小和问题
  • 逆序对问题

5.8 快速排序

快速排序的原理:

  1. 通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小
  2. 然后按照同样的方法对两部分分别进行排序,直到所有元素都排序完成
def quick_sort(list1, left, right):
    """
    快速排序
    """
    if left < right:
        # 区间上随机选择一个位置的元素进行划分
        key = list1[random.randint(left, right)]
        part = partition(list1, left, right, key)
        # 小于区继续排序
        quick_sort(list1, left, part[0]-1)
        # 大于区继续排序
        quick_sort(list1, part[1]+1, right)


def partition(list1, left, right, key):
    """
    根据key,对list1在[left,right]区间上的元素进行划分
    """
    less = left - 1
    more = right + 1
    index = left
    while index < more:
        if list1[index] < key:
            less += 1
            swap(list1, less, index)
            index += 1
        elif list1[index] > key:
            more -= 1
            swap(list1, more, index)
        else:
            index += 1
    # 返回等于区的位置
    return less+1, more-1
  • 经典快速排序
    • 最优时间复杂度 O(N*logN)
    • 最差时间复杂度O(N^2) ,会退化为冒泡排序
  • 随机快速排序
    • 长期期望的时间复杂度O(N*logN)

相关题目:

  • 荷兰国旗问题 leetcode 75
  • 数据的奇偶性划分
  • 无序列表中第K小元素

6. 排序的稳定性

排序的稳定性是指:当列表内存在相同的元素,排序完成后,相同的元素前后位置不会发生改变,此排序可以做到排序稳定。

注:

  • 冒泡排序、插入排序、归并排序可以做到排序稳定性
  • 选择排序、快速排序不符合排序稳定性
  • 快速排序理论上可以做到稳定性,参考论文《01 stable quick sort》