志 李華
摘要:計算質數個數,最常用的是質數定理函數x/ln(x)和高斯函數Li(x)。但是兩者對質數個數的估計前者總是小于,后者總是大于質數的實際個數,并且隨著數量級的增加,偏差擴大。前文已經提出了對x/ln(x)進行改進的函數p(x)。現對高斯函數Li(x)進行動態修正,得到了一個更為理想的質數個數估計函數 q(x)。數值實驗表明,與p(x)和黎曼函數R(x)計算結果比較,改進后的q(x)計算簡便,且精確度較高。
關鍵詞:質數定理 質數個數 質數計數函數 動態修正
計算質數個數,最常用的是質數定理函數x/ln(x)和高斯函數Li(x)(1)。高斯函數 Li(x) 表示為(2):
高斯在1792年,勒讓德在1798年就發現了質數定理(2)。但是質數定理函數和高斯函數對質數個數的估計均存在較大偏差。前文已經對x/ln(x)進行了改進,獲得了改進的質數估計函數p(x)(3),表示為:
其中:n=1.2為質數函數常數。
黎曼函數R(x) 的一種表示是(4):
其中:
是黎曼 函數。
黎曼函數的計算復雜,在給定數量級非常大時與Li(x)同樣偏離真實值(4)。因此有必要探索更為精確的質數計數函數。
質數的分布類型屬于確定性隨機分布(5)。因此,理論上存在能夠相對精確表示質數個數的函數。
現對高斯函數Li(x)進行動態修正,到了一個較理想的質數個數估計公式q(x)。數值實驗表明,改進后的q(x)與p(x)和R(x)計算結果比較,它計算簡便,且精確度較高。
實驗觀察可知,高斯函數Li(x)計算數值偏差較大,并總是大于實際的質數計數。本文對高斯函數Li(x)進行動態修正,在其積分式的分母上添加一個適當的正的函數,以減少積分的值,使其更接近實際的質數計數。經過大量實驗,并進一步優化,確定了函數表達式,給出如下定義。
定義q(x)為修正后的質數計數函數:
。
應用q(x)計算小于給定數量級的質數個數,并與應用p(x)和R(x)的計算結果進行比較,詳見表1~表2和以及圖1~圖9。實驗表明,很多情況下q(x)與 p(x) 和 R(x) 非常近似于質數計數函數π(x),并隨著 x 的增大,q(x)與 p(x) 和 R(x) 以及π(x) 互有交叉, q(x)表現了良好的逼近性能。q(x)函數形式簡單,計算方便,且具有相對較高精確度。
注:x為整數,π(x)為質數的實際個數函數,p(x)為改進的質數計數函數,R(x)為黎曼質數計數函數,q(x)為本文改進的質數計數函數。
圖1 棕色Li(x),綠色q(x),紅色p(x),藍色π(x),黑色R(x),紫色x/ln(x)。
圖2 棕色Li(x),綠色q(x),紅色p(x),藍色π(x),黑色R(x),紫色x/ln(x)。
圖3 綠色q(x),紅色p(x),藍色π(x),黑色R(x),紫色x/ln(x)。
圖4 綠色q(x),紅色p(x),藍色π(x),黑色R(x)。
圖5 綠色q(x),紅色p(x),藍色π(x),黑色R(x)。
圖6 綠色q(x),紅色p(x),藍色π(x),黑色R(x)。
圖7 綠色q(x),紅色p(x),藍色π(x),黑色R(x)。
圖8 綠色q(x),紅色p(x),藍色π(x),黑色R(x)。
圖9 綠色q(x),紅色p(x),藍色π(x),黑色R(x)。
表1結果可見,應用q(x)計算小于給定數量級的質數個數相對較精確,優于應用p(x)和R(x)的計算結果。
表2結果可見,在給定區間內q(x)、p(x)和R(x)共有19組數據。q(x)與p(x)和R(x)計算值與π(x)計算值比較,q(x)與p(x)和R(x)變動方向完全一致,同步率達到100.00%;大于和小于π(x)計算值的分別為6組和13組,占31.58%和68.42%;其中q(x)有7組優于R(x),占比達36.84%;q(x)有14組優于p(x),占比達73.68%。
q(x)計算結果與p(x)和R(x)計算結果相近,但最大偏差和平均偏差均略優于R(x)。
圖1顯示了六個函數之間在區間[10,1000]上的關系,Li(x)總是大于、x/ln(x)總是小于 p(x),q(x),π(x),和R(x);而q(x),π(x),R(x)三者相互纏繞。圖2~圖9結果顯示,q(x)與p(x)和R(x)的函數曲線與π(x)的函數曲線出現多處糾纏和穿越,表明計算不同區間質數個數時,會出現q(x)與p(x)和R(x)交替為優的情況。另外,q(x)與p(x)、π(x)和R(x)的函數曲線非常接近,擬合較佳。在圖4以及之后,由于x/ln(x)偏差較大,在圖上已經不再出現。
因為質數的分布存在一定的隨機性,實際質數個數始終在q(x)函數值的上下浮動,頻繁變化,呈現波動狀態。因此q(x)的函數曲線可稱為質數曲線,q(x)可稱為質數計數函數。
易知q(x)計算簡便,精確度高于p(x)和R(x)的計算結果。q(x)是一個較為理想的質數計數函數,具有廣泛的應用前景。
[1] G. H.Hardy and E. M. Wright. An Introduction to the Theory of Numbers, 6th ed., Oxford University Press, 2008
[2] https://mathworld.wolfram.com/PrimeCountingFunction.html
[3] Zhi Li and Hua Li.A Revised Prime Number Counting Function. https://vixra.org/abs/2301.0104.
[4] https://primes.utm.edu/howmany.html
[5] Zhi Li and Hua Li.Proof of N^2+1 Conjecture. https://vixra.org/abs/2209.0059
62. Prime Number of Set Bits in Binary Representation
Given two integers L and R, find the count of numbers in the range [L, R] (inclusive) having a prime number of set bits in their binary representation.
(Recall that the number of set bits an integer has is the number of 1s present when written in binary. For example, 21 written in binary is 10101 which has 3 set bits. Also, 1 is not a prime.)
Example 1:
Input: L = 6, R = 10
Output: 4
Explanation:
6 -> 110 (2 set bits, 2 is prime)
7 -> 111 (3 set bits, 3 is prime)
9 -> 1001 (2 set bits , 2 is prime)
10->1010 (2 set bits , 2 is prime)
Example 2:
Input: L = 10, R = 15
Output: 5
Explanation:
10 -> 1010 (2 set bits, 2 is prime)
11 -> 1011 (3 set bits, 3 is prime)
12 -> 1100 (2 set bits, 2 is prime)
13 -> 1101 (3 set bits, 3 is prime)
14 -> 1110 (3 set bits, 3 is prime)
15 -> 1111 (4 set bits, 4 is not prime)
Note:
對應中文如下
給定兩個整數 L 和 R ,找到閉區間 [L, R] 范圍內,計算置位位數為質數的整數個數。
(注意,計算置位代表二進制表示中1的個數。例如 21 的二進制表示 10101 有 3 個計算置位。還有,1 不是質數。)
示例 1:
輸入: L = 6, R = 10
輸出: 4
解釋:
6 -> 110 (2 個計算置位,2 是質數)
7 -> 111 (3 個計算置位,3 是質數)
9 -> 1001 (2 個計算置位,2 是質數)
10-> 1010 (2 個計算置位,2 是質數)
示例 2:
輸入: L = 10, R = 15
輸出: 5
解釋:
10 -> 1010 (2 個計算置位, 2 是質數)
11 -> 1011 (3 個計算置位, 3 是質數)
12 -> 1100 (2 個計算置位, 2 是質數)
13 -> 1101 (3 個計算置位, 3 是質數)
14 -> 1110 (3 個計算置位, 3 是質數)
15 -> 1111 (4 個計算置位, 4 不是質數)
注意:
首先理解一下“計算置位代表二進制表示中1的個數”,就是轉化成二進制之后,其中1的個數。這里算法會有很多,首先想到的是,循環除2,算出1的個數。
var binaryRepresentation = function(i) {
var result = 0;
while(i > 0){
result += i & 1;
i = i >>> 1;
}
return result;
}
然后這邊有限制最大值,10^6, 小于 2^21, 即最大21,涉及到的質數羅列如下 2,3,5,7,11,13,17,19。同時想到一個利用與運算的方式,快速計算1的個數。
var countPrimeSetBits = function(L, R) {
var result = 0;
var map = {
1: 0,
2: 1,
3: 1,
4: 0,
5: 1,
6: 0,
7: 1,
8: 0,
9: 0,
10: 0,
11: 1,
12: 0,
13: 1,
14: 0,
15: 0,
16: 0,
17: 1,
18: 0,
19: 1,
20: 0,
21: 0,
22: 0,
23: 1
}
for(var i = L; i <= R; i++){
result += map[binaryRepresentation(i)];
}
return result;
};
以上即為JavaScript的解法,這道題目并不困難,理解題目意思之后,再結合一些限制條件,很快就能給出對應算法。
計劃開一個新的系列,來講一講在工作中經常用到的性能優化手段、思路和如何發現性能瓶頸,后續有時間的話應該會整理一系列的博文出來。
今天要談的一個性能優化的Tips是一個老生常談的點,但是也是很多人沒有注意的一個點。在使用集合類型是,你應該設置一個預估的初始大小,那么為什么需要這樣做?我們一起來從源碼的角度說一說。
我們先來聊一聊.NET BCL庫中提供的集合類型,對于這個大家肯定都不陌生,比如List、HashSet、Dictionary、Queue、Stack等等,這些都是大家每天都用到,非常熟悉的類型了,那么大家在使用的時候有沒有注意過它們有一個特殊構造函數呢?像下面代碼塊中的那樣。
public Stack (int capacity)
public List (int capacity)
public Queue (int capacity)
public HashSet (int capacity)
public Dictionary (int capacity)
哎?為什么這些構造函數都有一個叫capacity的參數呢?我們來看看這個參數的注釋。初始化類的新實例,該實例為空并且具有指定的初始容量或默認初始容量。
這就很奇怪了不是嗎?在我們印象里面只有數組之類的才需要指定固定的長度,為什么這些可以無限添加元素的集合類型也要設置初始容量呢?這其實和這些集合類型的實現方式有關,廢話不多說,我們直接看源碼。
首先來看比較簡單的List的源碼(源碼地址在文中都做了超鏈接,可以直接點擊過去,在文末也會附上鏈接地址)。下面是List的私有變量。
// 用于存在實際的數據,添加進List的元素都由存儲在_items數組中
internal T[] _items;
// 當前已經存儲了多少元素
internal int _size;
// 當前的版本號,List每發生一次元素的變更,版本號都會+1
private int _version;
從上面的源碼中,我們可以看到雖然List是動態集合,可以無限的往里面添加元素,但是它底層存儲數據的還是使用的數組,那么既然使用的數組那它是怎么實現能無限的往里面添加元素的?我們來看看Add方法。
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(T item)
{
// 版本號+1
_version++;
T[] array = _items;
int size = _size;
// 如果當前已經使用的空間 小于數組大小那么直接存儲 size+1
if ((uint)size < (uint)array.Length)
{
_size = size + 1;
array[size] = item;
}
else
{
// 注意!! 如果已經使用的空間等于數組大小,那么走AddWithResize方法
AddWithResize(item);
}
}
從上面的源碼可以看到,如果內部_item數組有足夠的空間,那么元素直接往里面加就好了,但是如果內部已存放的元素_size等于_item數組大小時,會調用AddWithResize方法,我們來看看里面做了啥。
// AddWithResize方法
[MethodImpl(MethodImplOptions.NoInlining)]
private void AddWithResize(T item)
{
Debug.Assert(_size == _items.Length);
int size = _size;
// 調用Grow方法,并且把size+1,至少需要size+1的空間才能完成Add操作
Grow(size + 1);
_size = size + 1;
_items[size] = item;
}
// Grow方法
private void Grow(int capacity)
{
Debug.Assert(_items.Length < capacity);
// 如果內部數組長度等于0,那么賦值為DefaultCapacity(大小為4),否則就賦值兩倍當前長度
int newcapacity = _items.Length == 0 ? DefaultCapacity : 2 * _items.Length;
// 這里做了一個判斷,如果newcapacity大于Array.MaxLength(大小是2^31元素)
// 也就是說一個List最大能存儲2^32元素
if ((uint)newcapacity > Array.MaxLength) newcapacity = Array.MaxLength;
// 如果newpapacity小于預算需要的容量,也就是說元素數量大于Array.MaxLength
// 后面Capacity會拋出異常
if (newcapacity < capacity) newcapacity = capacity;
// 為Capacity屬性設置值
Capacity = newcapacity;
}
// Capacity屬性
public int Capacity
{
// 獲取容量,直接返回_items的容量
get => _items.Length;
set
{
// 如果value值還小于當前元素個數
// 直接拋異常
if (value < _size)
{
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_SmallCapacity);
}
// 如果value值和當前數組長度不匹配
// 那么走擴容邏輯
if (value != _items.Length)
{
// value大于0新建一個新的數組
// 將原來的數組元素拷貝過去
if (value > 0)
{
T[] newItems = new T[value];
if (_size > 0)
{
Array.Copy(_items, newItems, _size);
}
_items = newItems;
}
// value小于0 那么直接把_items賦值為空數組
// 原本的數組可以被gc回收了
else
{
_items = s_emptyArray;
}
}
}
通過上面的代碼我們可以得到兩個有意思的結論。
一個List元素最大能存放2^31個元素.不設置Capacity的話,默認初始大小為4,后面會以2倍的空間擴容。
List底層是通過數組來存放元素的,如果空間不夠會按照2倍大小來擴容,但是它并不能無限制的存放數據。
在元素低于4個的情況下,不設置Capacity不會有任何影響;如果大于4個,那么就會走擴容流程,不僅需要申請新的數組,而且還要發生內存復制和需要GC回收原來的數組。
大家必須知道分配內存、內存復制和GC回收內存的代價是巨大的,下面有個示意圖,舉了一個從4擴容到8的例子。
上面列舉了我們從源碼中看到的情況,那么不設置初始化的容量,對性能影響到底有多大呢?所以構建了一個Benchmark,來看看在不同量級下的影響,下面的Benchmark主要是探究兩個問題。
public class ListCapacityBench
{
// 宇宙的真理 42
private static readonly Random OriginRandom = new(42);
// 整一個數列模擬常見的集合大小 最大12萬
private static readonly int[] Arrays =
{
3, 5, 8, 13, 21, 34, 55, 89, 100, 120, 144, 180, 200, 233,
250, 377, 500, 550, 610, 987, 1000, 1500, 1597, 2000, 2584,
4181, 5000, 6765, 10946, 17711, 28657, 46368, 75025, 121393
};
// 生成一些隨機數
private static readonly int[] OriginArrays = Enumerable.Range(0, Arrays.Max()).Select(c => OriginRandom.Next()).ToArray();
// 不設置容量
[Benchmark(Baseline = true)]
public int WithoutCapacity()
{
return InnerTest(null);
}
// 剛好設置需要的容量
[Benchmark]
public int SetArrayLengthCapacity()
{
return InnerTest(null, true);
}
// 設置為8
[Benchmark]
public int Set8Capacity()
{
return InnerTest(8);
}
// 設置為16
[Benchmark]
public int Set16Capacity()
{
return InnerTest(16);
}
// 設置為32
[Benchmark]
public int Set32Capacity()
{
return InnerTest(32);
}
// 設置為64
[Benchmark]
public int Set64Capacity()
{
return InnerTest(64);
}
// 實際的測試方法
// 不使用JIT優化,模擬集合的實際使用場景
[MethodImpl(MethodImplOptions.NoOptimization)]
private static int InnerTest(int? capacity, bool setLength = false)
{
var list = new List<int>();
foreach (var length in Arrays)
{
List<int> innerList;
if (capacity == null)
{
innerList = setLength ? new List<int>(length) : new List<int>();
}
else
{
innerList = new List<int>(capacity.Value);
}
// 真正的測試方法 簡單的填充數據
foreach (var item in OriginArrays.AsSpan()[..length])
{
innerList.Add(item);
}
list.Add(innerList.Count);
}
return list.Count;
}
從上面的Benchmark結果可以看出來,設置剛好需要的初始容量最快(比不設置快40%)、GC次數最少(50%+)、分配的內存也最少(節約60%),另外不建議不設置初始大小,它是最慢的。
要是實在不能預估大小,那么可以無腦設置一個8表現稍微好一點點。如果能預估大小,因為它是2倍擴容,可以在2的N次方中找一個接近的。
8 16 32 64 128 512 1024 2048 4096 8192 ......
接下來看看Queue和Stack,看看它的擴容邏輯是怎么樣的。
private void Grow(int capacity)
{
Debug.Assert(_array.Length < capacity);
const int GrowFactor = 2;
const int MinimumGrow = 4;
int newcapacity = GrowFactor * _array.Length;
if ((uint)newcapacity > Array.MaxLength) newcapacity = Array.MaxLength;
newcapacity = Math.Max(newcapacity, _array.Length + MinimumGrow);
if (newcapacity < capacity) newcapacity = capacity;
SetCapacity(newcapacity);
}
基本一樣,也是2倍擴容,所以按照我們上面的規則就好了。
HashSet和Dictionary的邏輯實現類似,只是一個Key就是Value,另外一個是Key對應Value。不過它們的擴容方式有所不同,具體可以看我之前的博客,來看看擴容的源碼,這里以HashSet為例。
private void Resize() => Resize(HashHelpers.ExpandPrime(_count), forceNewHashCodes: false);
private void Resize(int newSize, bool forceNewHashCodes)
{
// Value types never rehash
Debug.Assert(!forceNewHashCodes || !typeof(T).IsValueType);
Debug.Assert(_entries != null, "_entries should be non-null");
Debug.Assert(newSize >= _entries.Length);
var entries = new Entry[newSize];
int count = _count;
Array.Copy(_entries, entries, count);
if (!typeof(T).IsValueType && forceNewHashCodes)
{
Debug.Assert(_comparer is NonRandomizedStringEqualityComparer);
_comparer = (IEqualityComparer<T>)((NonRandomizedStringEqualityComparer)_comparer).GetRandomizedEqualityComparer();
for (int i = 0; i < count; i++)
{
ref Entry entry = ref entries[i];
if (entry.Next >= -1)
{
entry.HashCode = entry.Value != null ? _comparer!.GetHashCode(entry.Value) : 0;
}
}
if (ReferenceEquals(_comparer, EqualityComparer<T>.Default))
{
_comparer = null;
}
}
// Assign member variables after both arrays allocated to guard against corruption from OOM if second fails
_buckets = new int[newSize];
#if TARGET_64BIT
_fastModMultiplier = HashHelpers.GetFastModMultiplier((uint)newSize);
#endif
for (int i = 0; i < count; i++)
{
ref Entry entry = ref entries[i];
if (entry.Next >= -1)
{
ref int bucket = ref GetBucketRef(entry.HashCode);
entry.Next = bucket - 1; // Value in _buckets is 1-based
bucket = i + 1;
}
}
_entries = entries;
}
從上面的源碼中可以看到Resize的步驟如下所示。
從這里大家就可以看出來,如果不指定初始大小的話,HashSet和Dictionary這樣的數據結構擴容的成本更高,因為還需要ReHash這樣的操作。
那么HashHelpers.ExpandPrime是一個什么樣的方法呢?究竟每次會擴容多少空間呢?我們來看HashHelpers源碼。
public const uint HashCollisionThreshold = 100;
// 這是比Array.MaxLength小最大的素數
public const int MaxPrimeArrayLength = 0x7FFFFFC3;
public const int HashPrime = 101;
public static int ExpandPrime(int oldSize)
{
// 新的size等于舊size的兩倍
int nwSize = 2 * oldSize;
// 和List一樣,如果大于了指定最大值,那么直接返回最大值
if ((uint)newSize > MaxPrimeArrayLength && MaxPrimeArrayLength > oldSize)
{
Debug.Assert(MaxPrimeArrayLength == GetPrime(MaxPrimeArrayLength), "Invalid MaxPrimeArrayLength");
return MaxPrimeArrayLength;
}
// 獲取大于newSize的第一素數
return GetPrime(newSize);
}
public static int GetPrime(int min)
{
if (min < 0)
throw new ArgumentException(SR.Arg_HTCapacityOverflow);
// 獲取大于min的第一個素數
foreach (int prime in s_primes)
{
if (prime >= min)
return prime;
}
// 如果素數列表里面沒有 那么計算
for (int i = (min | 1); i < int.MaxValue; i += 2)
{
if (IsPrime(i) && ((i - 1) % HashPrime != 0))
return i;
}
return min;
}
// 用于擴容的素數列表
private static readonly int[] s_primes =
{
3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919,
1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591,
17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437,
187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263,
1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369
};
// 當容量大于7199369時,需要計算素數
public static bool IsPrime(int candidate)
{
if ((candidate & 1) != 0)
{
int limit = (int)Math.Sqrt(candidate);
for (int divisor = 3; divisor <= limit; divisor += 2)
{
if ((candidate % divisor) == 0)
return false;
}
return true;
}
return candidate == 2;
}
從上面的代碼我們就可以得出HashSet和Dictionary每次擴容后的大小就是大于二倍Size的第一個素數,和List直接擴容2倍還是有差別的。
至于為什么要使用素數來作為擴容的大小,簡單來說是使用素數能讓數據在Hash以后更均勻的分布在各個桶中(素數沒有其它約數),這不在本文的討論范圍,具體可以戳鏈接1、鏈接2、鏈接3了解更多。
從性能的角度來說,強烈建議大家在使用集合類型時指定初始容量,總結了下面的幾個點。
文章來自https://www.cnblogs.com/InCerry/p/Dotnet-Opt-Perf-You-Should-Set-Capacity-For-Collection.html
*請認真填寫需求信息,我們會在24小時內與您取得聯系。