시간 복잡도는 낮지 만 프로그램 실행 속도는 느립니까?

어제 알고리즘 문제가 발생했으며 시간 복잡성이 낮은 알고리즘 프로그램이 더 느리게 실행된다는 것을 발견했습니다. 분석하는 동안 프로그램과 알고리즘의 성능에 대해 더 깊이 이해했습니다. 다음은 Go 언어 구현에 한합니다. 제출은 영어 leetcode 웹 사이트에 있습니다. 결과는 중국어 웹 사이트와 다릅니다. 계산 방법에 차이가 있는지 모르겠습니다.

Leetcode 질문 239- 데이터의 최대 슬라이딩 윈도우

난폭 한 해결책 만 생각했는데, 매번 미끄러 져 버리고 새 값을 추가하면 창의 최대 값이 판단되고 시간 복잡도는 O ((n-k + 1) * k)입니다. 공식은 선형 시간 복잡성을 구현하기 위해 deque를 권장 했으며 , Xiaohao 알고리즘 은 폭력적인 솔루션의 성능에 대한 나쁜 결과를 봅니다.

그러나 deque AC를 사용한 후 가장 빠른 답변은 폭력적인 방법으로 밝혀졌습니다. leetcode 계산이 정확하지 않습니까? 이를 검증하기 위해 벤치 마크를 수행하겠습니다.


이것은 대답에서 가장 빠른 무차별 대입 알고리즘입니다.

func maxSlidingWindow(nums []int, k int) []int {
	res := make([]int, (len(nums)+1)-k)
	max := -10000

	for i := 0; i <= len(nums)-k; i++ {
		if max > -10000 && nums[i-1] != max && nums[i+k-1] < max {
			res[i] = max
			continue
		} else {
			max = -10000
		}
		for _, v := range nums[i : i+k] {
			if v > max {
				max = v
			}
		}
		res[i] = max
	}
	return res
}

간단한 사용 사례를 사용하여 테스트합니다.

func Benchmark_maxSlidingWindow(b *testing.B) {
	a := []int{9, 10, 9, -7, -4, -8, 2, -6}
	for i := 0; i < b.N; i++ {
		_ = maxSlidingWindow(a, 5)
	}
}

결과는 38.4ns / op입니다.

Benchmark_maxSlidingWindow-8   	27957754	        38.4 ns/op	      32 B/op	       1 allocs/op

이것은 deque의 알고리즘입니다.

func maxSlidingWindow(nums []int, k int) []int {
	res := make([]int, (len(nums)+1)-k)
	win := make([]int, 0, k)
	for i, v := range nums {

		for len(win) > 0 && win[len(win)-1] < v {
			win = win[:len(win)-1]
		}
		win = append(win, v)
		if i >= k && win[0] == nums[i-k] {
			win = win[1:]
		}
		if i+1 >= k {
			res[i-k+1] = win[0]
		}
	}
	return res
}

결과는 172ns / op입니다.

Benchmark_maxSlidingWindow-8   	 6647554	       172 ns/op	     112 B/op	       6 allocs/op

무차별 대입 알고리즘은 실제로 훨씬 더 빠릅니다. 벤치 마크의 결과는 저에게 영감을주었습니다. 데크는 6 개의 메모리 할당을 수행했고 무차별 대입 솔루션은 한 번만 수행되었습니다. 이로 인해 메모리 소비가 증가 할뿐만 아니라 알고리즘 시간 복잡성이 낮아 지지만 프로그램 시간이 길어집니다.

deque의 알고리즘에서 두 개의 슬라이스가 생성되고 슬라이스에 요소를 추가하는 작업은 모두 append이므로 프로그램이 다중 메모리 할당 및 복사를 수행합니다. 무차별 대입 솔루션은 슬라이스를 생성하고 길이를 선언하기 만합니다. 수정시 요소에 값을 직접 할당하고 append메모리 할당 작업이 필요 하지 않습니다.


deque 방법에 대한 몇 가지 최적화 :

func maxSlidingWindow(nums []int, k int) []int {
	res := make([]int, (len(nums)+1)-k)
	win := make([]int, 0, k)
	for i, v := range nums {
		if i >= k && win[0] == i-k {
			win = win[1:]
		}
		for len(win) > 0 && nums[win[len(win)-1]] <= v {
			win = win[:len(win)-1]
		}
		win = append(win, i)
		if i+1 >= k {
			res[i-k+1] = nums[win[0]]
		}
	}
	return res
}

결과는 67.0ns / op로, 시간 소비를 60 % 줄입니다.

Benchmark_maxSlidingWindow-8   	17957082	        67.0 ns/op	      80 B/op	       2 allocs/op

속도는 훨씬 빠르지 만 슬라이스가 하나 더 생성되기 때문에 무차별 대입 방법보다 여전히 느립니다.

개선 전 프로그램의 pprof 분석을 통해 메모리 및 슬라이스 확장 작업이 알고리즘 코드 실행보다 CPU 시간을 더 많이 소비하고 알고리즘 성능이 프로그램 실행의 전체 성능에 미치는 영향이 적음을 알 수 있습니다. .

  flat  flat%   sum%        cum   cum%
 0.33s 23.24% 23.24%      0.83s 58.45%  runtime.mallocgc
 0.26s 18.31% 41.55%      1.18s 83.10%  runtime.growslice
 0.22s 15.49% 57.04%      1.42s   100%  algorithm/sliding_window.maxSlidingWindow
 0.15s 10.56% 67.61%      0.15s 10.56%  runtime.nextFreeFast (inline)
 0.07s  4.93% 72.54%      0.07s  4.93%  runtime.memclrNoHeapPointers
 0.05s  3.52% 76.06%      0.05s  3.52%  runtime.pageIndexOf (inline)
 0.04s  2.82% 78.87%      0.04s  2.82%  runtime.memmove
 0.04s  2.82% 81.69%      0.04s  2.82%  runtime.releasem (inline)
 0.02s  1.41% 83.10%      0.02s  1.41%  runtime.(*mspan).objIndex (inline)
 0.02s  1.41% 84.51%      0.02s  1.41%  runtime.(*spanSet).pop

어제 Go language Chinese 웹 사이트 에서 "Learning Rob Pike의 6 가지 프로그래밍 원칙"기사를 보았습니다 . "알고리즘 복잡성과 처리 된 데이터 양 간의 관계"에 대한 견해 중 하나는 좋은 요약입니다.

Unix의 아버지이자 Go의 공동 창립자 인 Ken Thompson은 원칙 3과 4를 더욱 강조했습니다. 확실하지 않다면 철저히 말하십시오. 간단하고 폭력적인 방법이 종종 최선의 선택입니다. 보통 빠른 정렬이 "가장 빠른"정렬 알고리즘이라고 생각하지만 매일 발생하는 문제는 일반적으로 정렬 할 숫자가 적습니다. 이때 간단한 버블 정렬이 더 적합합니다. 관심있는 독자라면 프로그래밍 언어의 정렬 알고리즘이 구현되어 있으며 데이터 양에 따라 다른 정렬 알고리즘이 선택됩니다. 예를 들어 Go 언어 정렬 알고리즘의 구현은 요소 수가 12 개를 초과 할 때 빠른 정렬을 사용합니다.

추천

출처blog.csdn.net/qq_35753140/article/details/108033700