Tìm trung bình chạy từ một luồng số nguyên


223

Có thể trùng lặp:
Thuật toán trung bình cán trong C

Cho rằng số nguyên được đọc từ một luồng dữ liệu. Tìm trung bình của các yếu tố đọc cho đến nay theo cách hiệu quả.

Giải pháp tôi đã đọc: Chúng ta có thể sử dụng một heap tối đa ở bên trái để biểu diễn các phần tử nhỏ hơn trung vị hiệu quả và một heap tối thiểu ở bên phải để biểu diễn các phần tử lớn hơn phần tử trung bình hiệu quả.

Sau khi xử lý một phần tử đến, số phần tử trong heap khác nhau nhiều nhất là 1 phần tử. Khi cả hai heap chứa cùng một số phần tử, chúng ta sẽ thấy trung bình dữ liệu gốc của heap là trung bình hiệu quả. Khi các đống không cân bằng, chúng ta chọn trung vị hiệu quả từ gốc của heap chứa nhiều phần tử.

Nhưng làm thế nào chúng ta sẽ xây dựng một đống tối đa và tối thiểu heap tức là làm thế nào chúng ta biết được trung vị hiệu quả ở đây? Tôi nghĩ rằng chúng ta sẽ chèn 1 phần tử trong heap tối đa và sau đó là phần tử tiếp theo trong heap, và cứ như vậy cho tất cả các phần tử. Sửa tôi nếu tôi sai ở đây.


10
Thuật toán thông minh, sử dụng đống. Từ tiêu đề tôi không thể nghĩ ngay đến một giải pháp.
Vịt Mooing

1
Giải pháp của tể tướng có vẻ tốt với tôi, ngoại trừ việc tôi cho rằng (mặc dù bạn không nói rõ) rằng luồng này có thể dài tùy ý, vì vậy bạn không thể giữ mọi thứ trong bộ nhớ. Có phải vậy không?
Chạy hoang dã

2
@RastyWild Đối với các luồng dài tùy ý, bạn có thể lấy trung vị của các phần tử N cuối cùng bằng cách sử dụng các heap Fibros (để bạn xóa log (N)) và lưu trữ các con trỏ vào các phần tử được chèn theo thứ tự (ví dụ: deque), sau đó xóa phần cũ nhất phần tử ở mỗi bước một khi các heap đã đầy (cũng có thể di chuyển mọi thứ từ heap này sang heap khác). Bạn có thể nhận được phần nào tốt hơn N bằng cách lưu trữ số lượng phần tử lặp lại (nếu có nhiều lặp lại), nhưng nói chung, tôi nghĩ rằng bạn phải đưa ra một số giả định phân phối nếu bạn muốn trung bình của toàn bộ luồng.
Dougal

2
Bạn có thể bắt đầu với cả đống trống. Int đầu tiên đi trong một đống; thứ hai đi trong cái khác, hoặc bạn di chuyển mục đầu tiên sang heap khác và sau đó chèn. Điều này khái quát hóa cho "không cho phép một đống lớn hơn +1 khác" và không cần vỏ đặc biệt ("giá trị gốc" của một đống trống có thể được định nghĩa là 0)
Jon Watte

TÔI CHỈ có câu hỏi này trong một cuộc phỏng vấn MSFT. Cảm ơn bạn đã đăng bài
R Claven

Câu trả lời:


383

Có một số giải pháp khác nhau để tìm chạy trung bình từ dữ liệu được truyền phát, tôi sẽ nói ngắn gọn về chúng ở cuối câu trả lời.

Câu hỏi là về các chi tiết của một giải pháp cụ thể (giải pháp heap tối đa / phút heap) và cách giải quyết dựa trên heap hoạt động được giải thích dưới đây:

Đối với hai phần tử đầu tiên, thêm một phần tử nhỏ hơn vào maxHeap ở bên trái và phần tử lớn hơn cho minHeap ở bên phải. Sau đó xử lý từng luồng dữ liệu,

Step 1: Add next item to one of the heaps

   if next item is smaller than maxHeap root add it to maxHeap,
   else add it to minHeap

Step 2: Balance the heaps (after this step heaps will be either balanced or
   one of them will contain 1 more item)

   if number of elements in one of the heaps is greater than the other by
   more than 1, remove the root element from the one containing more elements and
   add to the other one

Sau đó, tại bất kỳ thời điểm nào, bạn có thể tính toán trung vị như thế này:

   If the heaps contain equal amount of elements;
     median = (root of maxHeap + root of minHeap)/2
   Else
     median = root of the heap with more elements

Bây giờ tôi sẽ nói về vấn đề nói chung như đã hứa trong phần đầu của câu trả lời. Tìm kiếm trung bình chạy từ một luồng dữ liệu là một vấn đề khó khăn và việc tìm một giải pháp chính xác với các hạn chế về bộ nhớ một cách hiệu quả có lẽ là không thể đối với trường hợp chung. Mặt khác, nếu dữ liệu có một số đặc điểm chúng ta có thể khai thác, chúng ta có thể phát triển các giải pháp chuyên dụng hiệu quả. Ví dụ: nếu chúng ta biết rằng dữ liệu là một loại tích phân, thì chúng ta có thể sử dụng sắp xếp đếm, có thể cung cấp cho bạn một thuật toán thời gian liên tục bộ nhớ không đổi. Giải pháp dựa trên Heap là một giải pháp tổng quát hơn vì nó cũng có thể được sử dụng cho các loại dữ liệu khác (nhân đôi). Và cuối cùng, nếu không yêu cầu trung bình chính xác và xấp xỉ là đủ, bạn chỉ cần cố gắng ước tính hàm mật độ xác suất cho dữ liệu và ước tính trung bình sử dụng.


6
Các đống này phát triển mà không bị ràng buộc (tức là một cửa sổ 100 phần tử trượt trên 10 triệu phần tử sẽ yêu cầu tất cả 10 triệu phần tử được lưu trữ trong bộ nhớ). Xem bên dưới để biết câu trả lời khác bằng cách sử dụng skiplists chỉ mục mà chỉ yêu cầu 100 phần tử được nhìn thấy gần đây nhất được giữ trong bộ nhớ.
Raymond Hettinger

1
Bạn cũng có thể có một giải pháp bộ nhớ giới hạn bằng cách sử dụng heaps, như được giải thích trong một trong những ý kiến ​​cho chính câu hỏi.
Hakan Serce

1
Bạn có thể tìm thấy một triển khai của giải pháp dựa trên heap trong c ở đây.
HỎI

1
Wow điều này đã giúp tôi không chỉ giải quyết vấn đề cụ thể này mà còn giúp tôi học được rất nhiều thứ ở đây là cách triển khai cơ bản của tôi trong python: github.com/PythonAlgo/DataSturation
swati saoji 24/2/2016

2
@HakanSerce Bạn có thể giải thích lý do tại sao chúng tôi đã làm những gì chúng tôi đã làm? Tôi có nghĩa là tôi có thể thấy điều này hoạt động, nhưng tôi không thể hiểu nó bằng trực giác.
Shiva

51

Nếu bạn không thể giữ tất cả các mục trong bộ nhớ cùng một lúc, vấn đề này sẽ trở nên khó khăn hơn nhiều. Giải pháp heap yêu cầu bạn giữ tất cả các phần tử trong bộ nhớ cùng một lúc. Điều này là không thể trong hầu hết các ứng dụng trong thế giới thực của vấn đề này.

Thay vào đó, như bạn thấy con số, theo dõi các số của số lần bạn nhìn thấy mỗi số nguyên. Giả sử số nguyên 4 byte, đó là 2 ^ 32 xô, hoặc nhiều nhất là 2 ^ 33 số nguyên (khóa và số đếm cho mỗi int), là 2 ^ 35 byte hoặc 32GB. Nó có thể sẽ ít hơn nhiều so với điều này bởi vì bạn không cần lưu trữ khóa hoặc tính cho những mục nhập bằng 0 (ví dụ như một defaultdict trong python). Điều này mất thời gian liên tục để chèn từng số nguyên mới.

Sau đó, tại bất kỳ điểm nào, để tìm trung vị, chỉ cần sử dụng số đếm để xác định số nguyên nào là phần tử ở giữa. Điều này cần thời gian không đổi (mặc dù một hằng số lớn, nhưng dù sao cũng không đổi).


3
Nếu gần như tất cả các số được nhìn thấy một lần, thì một danh sách thưa thớt sẽ chiếm nhiều bộ nhớ hơn . Và có vẻ như nếu bạn có quá nhiều số thì chúng không khớp với số mà hầu hết các số sẽ xuất hiện một lần. Bỏ qua điều đó, đây là một giải pháp thông minh cho số lượng lớn các con số.
Vịt mướp

1
Đối với một danh sách thưa thớt, tôi đồng ý, điều này là tồi tệ hơn về bộ nhớ. Mặc dù nếu các số nguyên được phân phối ngẫu nhiên, bạn sẽ bắt đầu nhận được các bản sao sớm hơn rất nhiều so với trực giác ngụ ý. Xem mathworld.wolfram.com/B birthdayPro Hiệu.html . Vì vậy, tôi khá chắc chắn rằng điều này sẽ có hiệu lực ngay khi bạn có thậm chí một vài GB dữ liệu.
Andrew C

4
@AndrewC bạn có thể vui lòng giải thích làm thế nào sẽ mất thời gian liên tục để tìm trung bình. Nếu tôi đã thấy n loại số nguyên khác nhau thì trong trường hợp xấu nhất, phần tử cuối cùng có thể là trung vị. Điều này làm cho trung bình tìm hoạt động O (n).
shshnk

@shshnk Không phải tổng số phần tử là >>> 2 ^ 35 trong trường hợp này sao?
VishAmdi

@shshnk Bạn nói đúng rằng nó vẫn tuyến tính với số lượng các số nguyên khác nhau mà bạn đã thấy, như VishAmdi nói, giả định tôi đưa ra cho giải pháp này là n là số lượng bạn đã thấy, rất nhiều số lớn hơn 2 ^ 33. Nếu bạn không thấy nhiều số đó, giải pháp maxheap chắc chắn tốt hơn.
Andrew C

49

Nếu phương sai của đầu vào được phân phối theo thống kê (ví dụ: bình thường, log-normal, v.v.) thì lấy mẫu hồ chứa là một cách hợp lý để ước tính tỷ lệ phần trăm / trung vị từ một dòng số dài tùy ý.

int n = 0;  // Running count of elements observed so far  
#define SIZE 10000
int reservoir[SIZE];  

while(streamHasData())
{
  int x = readNumberFromStream();

  if (n < SIZE)
  {
       reservoir[n++] = x;
  }         
  else 
  {
      int p = random(++n); // Choose a random number 0 >= p < n
      if (p < SIZE)
      {
           reservoir[p] = x;
      }
  }
}

"hồ chứa" sau đó là một mẫu chạy, thống nhất (công bằng), mẫu của tất cả các đầu vào - bất kể kích thước. Tìm trung vị (hoặc bất kỳ phần trăm nào) sau đó là một vấn đề đơn giản trong việc phân loại hồ chứa và bỏ phiếu điểm thú vị.

Vì hồ chứa có kích thước cố định, nên loại này có thể được coi là O (1) một cách hiệu quả - và phương pháp này chạy với cả thời gian và mức tiêu thụ bộ nhớ không đổi.


Vì tò mò, tại sao bạn cần phương sai?
LazyCat

Luồng có thể trả về ít hơn các phần tử SIZE để cho một nửa hồ chứa trống. Điều này nên được xem xét khi tính toán trung vị.
Alex

Có cách nào để làm điều này nhanh hơn bằng cách tính toán sự khác biệt thay vì trung vị không? Là mẫu bị loại bỏ và thêm vào và trung bình trước đó đủ thông tin cho điều đó?
inf3rno

30

Cách hiệu quả nhất để tính phần trăm của luồng mà tôi đã tìm thấy là thuật toán P²: Raj Jain, Imrich Chlamtac: Thuật toán P² để tính toán động lượng tử và biểu đồ mà không cần lưu trữ quan sát. Cộng đồng. ACM 28 (10): 1076-1085 (1985)

Thuật toán là thẳng về phía trước để thực hiện và hoạt động rất tốt. Đó là một ước tính, tuy nhiên, vì vậy hãy ghi nhớ điều đó. Từ tóm tắt:

Một thuật toán heuristic được đề xuất cho tính toán động qf trung vị và các lượng tử khác. Các ước tính được tạo ra một cách linh hoạt khi các quan sát được tạo ra. Các quan sát không được lưu trữ; do đó, thuật toán có yêu cầu lưu trữ rất nhỏ và cố định bất kể số lượng quan sát. Điều này làm cho nó lý tưởng để thực hiện trong một chip lượng tử có thể được sử dụng trong các bộ điều khiển và máy ghi công nghiệp. Thuật toán được mở rộng hơn nữa để vẽ biểu đồ. Độ chính xác của thuật toán được phân tích.


2
Count-Min Sketch tốt hơn P ^ 2 ở chỗ nó cũng bị lỗi trong khi cái sau thì không.
sinoTrality

1
Cũng xem xét "Tính toán trực tuyến hiệu quả không gian của các bản tóm tắt lượng tử" của Greenwald và Khanna, cũng đưa ra các giới hạn lỗi và có yêu cầu bộ nhớ tốt.
Paul Chernoch

1
Ngoài ra, để biết cách tiếp cận xác suất, hãy xem bài đăng trên blog này: Research.neustar.biz/2013/09/16/iêu và bài báo mà nó đề cập ở đây: arxiv.org/pdf/1407.1121v1.pdf này được gọi là "Frugal Truyền phát "
Paul Chernoch 24/08/2015

27

Nếu chúng ta muốn tìm trung vị của n phần tử được nhìn thấy gần đây nhất, thì vấn đề này có một giải pháp chính xác chỉ cần n phần tử được nhìn thấy gần đây nhất được giữ trong bộ nhớ. Nó là nhanh và quy mô tốt.

Một skiplist có thể lập chỉ mục hỗ trợ chèn O (ln n), loại bỏ và tìm kiếm được lập chỉ mục các phần tử tùy ý trong khi duy trì thứ tự sắp xếp. Khi được kết hợp với hàng đợi FIFO theo dõi mục cũ nhất thứ n, giải pháp rất đơn giản:

class RunningMedian:
    'Fast running median with O(lg n) updates where n is the window size'

    def __init__(self, n, iterable):
        self.it = iter(iterable)
        self.queue = deque(islice(self.it, n))
        self.skiplist = IndexableSkiplist(n)
        for elem in self.queue:
            self.skiplist.insert(elem)

    def __iter__(self):
        queue = self.queue
        skiplist = self.skiplist
        midpoint = len(queue) // 2
        yield skiplist[midpoint]
        for newelem in self.it:
            oldelem = queue.popleft()
            skiplist.remove(oldelem)
            queue.append(newelem)
            skiplist.insert(newelem)
            yield skiplist[midpoint]

Dưới đây là các liên kết để hoàn thành mã làm việc (phiên bản lớp dễ hiểu và phiên bản trình tạo được tối ưu hóa với mã skiplist có thể lập chỉ mục được nội tuyến):


7
Nếu tôi hiểu đúng về nó, điều này chỉ cung cấp cho bạn một trung vị của N phần tử cuối cùng được nhìn thấy, không phải tất cả các phần tử cho đến thời điểm đó. Điều này có vẻ như là một giải pháp thực sự khéo léo cho hoạt động đó mặc dù.
Andrew C

16
Đúng. Câu trả lời nghe có vẻ như có thể tìm ra trung vị của tất cả các yếu tố bằng cách chỉ giữ n phần tử cuối cùng trong bộ nhớ - nói chung là không thể. Thuật toán chỉ tìm trung vị của n phần tử cuối cùng.
Hans-Peter Störr

8
Thuật ngữ "chạy trung bình" thường được sử dụng để chỉ trung vị của một tập hợp con dữ liệu. OP được sử dụng một thuật ngữ phổ biến theo cách không chuẩn.
Rachel Hettinger

18

Một cách trực quan để suy nghĩ về điều này là nếu bạn có một cây tìm kiếm nhị phân cân bằng đầy đủ, thì gốc sẽ là phần tử trung bình, vì sẽ có cùng một số phần tử nhỏ hơn và lớn hơn. Bây giờ, nếu cây không đầy thì điều này sẽ không hoàn toàn như vậy vì sẽ có các yếu tố bị thiếu ở cấp độ cuối cùng.

Vì vậy, những gì chúng ta có thể làm thay vào đó là có trung vị và hai cây nhị phân cân bằng, một cho các phần tử nhỏ hơn trung vị và một cho các phần tử lớn hơn trung vị. Hai cây phải được giữ ở cùng kích thước.

Khi chúng tôi nhận được một số nguyên mới từ luồng dữ liệu, chúng tôi so sánh nó với trung vị. Nếu nó lớn hơn trung vị, chúng ta thêm nó vào đúng cây. Nếu hai kích thước cây khác nhau nhiều hơn 1, chúng ta sẽ loại bỏ phần tử min của cây bên phải, biến nó thành trung vị mới và đặt trung vị cũ vào cây bên trái. Tương tự cho nhỏ hơn.


Làm thế nào bạn sẽ làm điều đó? "Chúng tôi loại bỏ yếu tố tối thiểu của cây bên phải"
Hengameh 14/07/2015

2
Tôi có nghĩa là cây tìm kiếm nhị phân, vì vậy phần tử min là tất cả các cách còn lại từ gốc.
Irene Papakonstantinou

7

Hiệu quả là một từ phụ thuộc vào ngữ cảnh. Giải pháp cho vấn đề này phụ thuộc vào số lượng truy vấn được thực hiện liên quan đến số lượng chèn. Giả sử bạn đang chèn N số và K lần vào cuối bạn quan tâm đến trung vị. Độ phức tạp của thuật toán dựa trên heap sẽ là O (N log N + K).

Hãy xem xét các phương án sau. Đặt các số trong một mảng và cho mỗi truy vấn, hãy chạy thuật toán chọn tuyến tính (sử dụng trục quicksort, giả sử). Bây giờ bạn có một thuật toán với thời gian chạy O (KN).

Bây giờ nếu K đủ nhỏ (truy vấn không thường xuyên), thuật toán sau thực sự hiệu quả hơn và ngược lại.


1
Trong ví dụ heap, tra cứu là thời gian không đổi, vì vậy tôi nghĩ rằng nó phải là O (N log N + K), nhưng quan điểm của bạn vẫn giữ.
Andrew C

Vâng, điểm tốt, sẽ chỉnh sửa này. Bạn đúng N log N vẫn là thuật ngữ hàng đầu.
Peteris

-2

Bạn không thể làm điều này chỉ với một đống? Cập nhật: không. Xem bình luận.

Bất biến: Sau khi đọc các 2*nđầu vào, heap min giữ số nlớn nhất trong số chúng.

Vòng lặp: Đọc 2 đầu vào. Thêm cả hai vào heap và loại bỏ min của heap. Điều này thiết lập lại bất biến.

Vì vậy, khi 2nđầu vào đã được đọc, min của heap là lớn thứ n. Sẽ cần phải có thêm một chút phức tạp để lấy trung bình hai yếu tố xung quanh vị trí trung bình và để xử lý các truy vấn sau một số lượng đầu vào lẻ.


1
Không hoạt động: bạn có thể bỏ những thứ mà sau đó hóa ra là gần đầu. Chẳng hạn, hãy thử thuật toán của bạn với các số từ 1 đến 100, nhưng theo thứ tự ngược lại: 100, 99, ..., 1.
zellyn

Cảm ơn, zellyn. Ngớ ngẩn của tôi để thuyết phục bản thân sự bất biến đã được thiết lập lại.
Darius Bacon
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.