简体   繁体   中英

Complexity Issue With Minimum Priority Queue

I'm a student in a data structures class, and my current assignment was to make a minimum priority queue, which I've done and tested, and as far as I can tell it works. My issue is that we're also required to time our classes and analyze their Big-O complexity; I designed a timing class (which I've done for past assignments), collected the data, and plotted it. My deleteMin and add methods should have complexity O(logn), and the findMin should have O(c), however my add method is returning O(c) for some reason. Since I've tested the minPQ repeatedly I suspect the issue has something to do with the way I'm timing, but I'm stumped and am hoping someone here can pick up on something I've overlooked.

TLD; My add method is running faster than it should and/or there is an issue with my methodology for testing the add method.

EDIT: Some additional info about how the timer works; basically it is just adding random numbers to the queue to make it the right size, and then timing how long it takes to add one more. It goes from size 2^startPow to 2^stopPow, and it repeats the timing for each size iterCount times and outputs the average.

Here's my queue class:

package assignment11;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Comparator;
import java.util.NoSuchElementException;

/**
 * Represents a priority queue of generically-typed items. 
 * The queue is implemented as a min heap. 
 * The min heap is implemented implicitly as an array.
 * 
 * @author Christopher Nielson
 * @uid u0777607
 */
@SuppressWarnings("unchecked")
public class PriorityQueue<AnyType> {

    private int currentSize;

    private AnyType[] array;

    private Comparator<? super AnyType> cmp;

    /**
     * Constructs an empty priority queue. Orders elements according
     * to their natural ordering (i.e., AnyType is expected to be Comparable)
     * AnyType is not forced to be Comparable.
     */

    public PriorityQueue() {
        currentSize = 0;
        cmp = null;
        array = (AnyType[]) new Object[10]; // safe to ignore warning
    }

    /**
     * Construct an empty priority queue with a specified comparator.
     * Orders elements according to the input Comparator (i.e., AnyType need not
     * be Comparable).
     */
    public PriorityQueue(Comparator<? super AnyType> c) {
        currentSize = 0;
        cmp = c;
        array = (AnyType[]) new Object[10]; // safe to ignore warning
    }

    /**
     * @return the number of items in this priority queue.
     */
    public int size() {
        return currentSize;
    }

    /**
     * Makes this priority queue empty.
     */
    public void clear() {
        currentSize = 0;
    }

    /**
     * @return the minimum item in this priority queue.
     * @throws NoSuchElementException if this priority queue is empty.
     * 
     * (Runs in constant time.)
     */
    public AnyType findMin() throws NoSuchElementException {
        if (currentSize == 0) {
            throw new NoSuchElementException();
        }
        return array[0];
    }


    /**
     * Removes and returns the minimum item in this priority queue.
     * 
     * @throws NoSuchElementException if this priority queue is empty.
     * 
     * (Runs in logarithmic time.)
     */
    public AnyType deleteMin() throws NoSuchElementException {
        if (currentSize == 0) {
            throw new NoSuchElementException();
        }
        AnyType tmp = array[0];

        array[0] = array[currentSize - 1];
        array[currentSize - 1] = null;
        --currentSize;

        downHeap(0);

        return tmp;
    }


    /**
     * Adds an item to this priority queue.
     * 
     * (Runs in logarithmic time.) Can sometimes terminate early.
     * 
     * @param x -- the item to be inserted
     */
    public void add(AnyType x) {
        if (currentSize == array.length) {
            AnyType[] tmp = array;
            array = (AnyType[]) new Object[array.length * 2];
            for (int currentIndex = 0; currentIndex < tmp.length; currentIndex++) {
                array[currentIndex] = tmp[currentIndex];
            }
        }
        array[currentSize] = x;
        ++currentSize;

        upHeap(currentSize - 1);
    }

    /**
     * Generates a DOT file for visualizing the binary heap.
     */
    public void generateDotFile(String filename) {
        try(PrintWriter out = new PrintWriter(filename)) {
            out.println("digraph Heap {\n\tnode [shape=record]\n");

            for(int i = 0; i < currentSize; i++) {
                out.println("\tnode" + i + " [label = \"<f0> |<f1> " + array[i] + "|<f2> \"]");
                if(((i*2) + 1) < currentSize)
                    out.println("\tnode" + i + ":f0 -> node" + ((i*2) + 1) + ":f1");
                if(((i*2) + 2) < currentSize)
                    out.println("\tnode" + i + ":f2 -> node" + ((i*2) + 2) + ":f1");
            }
            out.println("}");
        } catch (IOException e) {
            System.out.println(e);
        }
    }

    /**
     * Internal method for comparing lhs and rhs using Comparator if provided by the
     * user at construction time, or Comparable, if no Comparator was provided.
     */
    private int compare(AnyType lhs, AnyType rhs) {
        if (cmp == null) {
            return ((Comparable<? super AnyType>) lhs).compareTo(rhs); // safe to ignore warning
        }
        // We won't test your code on non-Comparable types if we didn't supply a Comparator

        return cmp.compare(lhs, rhs);
    }

    /**
     * Internal method to reheapify upward.
     * 
     * @param root  Item where reheapifying will begin
     */
    private void upHeap(int root) {
        // check if root is less than parent
        if (root >= 0 && compare(array[root], array[(root - 1) / 2]) < 0) {
            AnyType tmp = array[(root - 1) / 2];
            array[(root - 1) / 2] = array[root];
            array[root] = tmp;
            upHeap((root - 1) / 2);
        }
    }

    /**
     * Internal method to reheapify downward.
     * 
     * @param root  Item where reheapifying will begin.
     */
    private void downHeap(int root) {
        // check if left child is less than root
        if ((root * 2) + 1 < currentSize && array[(root * 2) + 1] != null && compare(array[(root * 2) + 1], array[root]) < 0) {
            // check if right child is less than left child
            if ((root * 2) + 2 < currentSize && array[(root * 2) + 2] != null && compare(array[(root * 2) + 2], array[(root * 2) + 1]) < 0) {
                // swap with right child
                AnyType tmp = array[root];
                array[root] = array[(root * 2) + 2];
                array[(root * 2) + 2] = tmp;
                downHeap((root * 2) + 2);
            } else {
                // swap with left child
                AnyType tmp = array[root];
                array[root] = array[(root * 2) + 1];
                array[(root * 2) + 1] = tmp;
                downHeap((root * 2) + 1);
            }
        } else if ((root * 2) + 2 < currentSize && array[(root * 2) + 2] != null && compare(array[(root * 2) + 2], array[root]) < 0) {
            // swap with right child
            AnyType tmp = array[root];
            array[root] = array[(root * 2) + 2];
            array[(root * 2) + 2] = tmp;
            downHeap((root * 2) + 2);
        }
    }

    // LEAVE IN for grading purposes
    public Object[] toArray() {    
        Object[] ret = new Object[currentSize];
        for(int i = 0; i < currentSize; i++) {
            ret[i] = array[i];
        }
        return ret;
    }
}

And here's my timing class:

package assignment11;

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.util.Random;

/**
 * @author Christopher Nielson
 * @uid u0777607
 */
public class PriorityQueueTimer {

    private static int startPow = 10;
    private static int stopPow = 24;
    private static int iterCount = 10000;
    private static Random rand;
    private static OutputStreamWriter fileOut;

    public static void main(String[] args) {
        timeAdd();
//      timeDeleteMin();
//      timeFindMin();
        System.out.println("Finished timing!");
    }

    /**
     * Times add method of PriorityQueue for different data sizes and outputs results to a file.
     */
    private static void timeAdd() {
        try {
            fileOut = new OutputStreamWriter(new FileOutputStream(new File("assignment11-addTimer.csv")));
            PriorityQueue<Integer> addTimer;
            for (int currentPow = startPow; currentPow <= stopPow; currentPow++) {
                int dataSize = (int) Math.pow(2,  currentPow);
                System.out.print("Timing add on datasize " + dataSize + " (Power: " + currentPow + ")...");
                long totalTime = 0;
                long stopTime;

                addTimer = new PriorityQueue<Integer>();
                rand = new Random(13);

                for (int currentRand = 0; currentRand < dataSize; currentRand++) {
                    addTimer.add(rand.nextInt());
                }

                long startTime = System.nanoTime();
                while (System.nanoTime() - startTime < 1000000){}

                for (int currentIter = 0; currentIter < iterCount; currentIter++) {
                    startTime = System.nanoTime();
                    addTimer.add(rand.nextInt());
                    stopTime = System.nanoTime();
                    addTimer.deleteMin();
                    totalTime += stopTime - startTime;
                }
                System.out.println("Finished!");
                fileOut.write(dataSize + "\t" + (totalTime / iterCount) + "\n");
                fileOut.flush();
            }
            fileOut.close();
        } catch(Exception e) {
            System.err.println(e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * Times deleteMin method of PriorityQueue for different data sizes and outputs results to a file.
     */
    private static void timeDeleteMin() {
        try {
            fileOut = new OutputStreamWriter(new FileOutputStream(new File("assignment11-deleteMinTimer.csv")));
            PriorityQueue<Integer> deleteTimer;
            for (int currentPow = startPow; currentPow <= stopPow; currentPow++) {
                int dataSize = (int) Math.pow(2,  currentPow);
                System.out.print("Timing deleteMin on datasize " + dataSize + " (Power: " + currentPow + ")...");
                long totalTime = 0;
                long stopTime;

                deleteTimer = new PriorityQueue<Integer>();
                rand = new Random(13);

                for (int currentRand = 0; currentRand < dataSize; currentRand++) {
                    deleteTimer.add(rand.nextInt());
                }

                long startTime = System.nanoTime();
                while (System.nanoTime() - startTime < 1000000){}

                for (int currentIter = 0; currentIter < iterCount; currentIter++) {
                    startTime = System.nanoTime();
                    deleteTimer.deleteMin();
                    stopTime = System.nanoTime();
                    deleteTimer.add(rand.nextInt());
                    totalTime += stopTime - startTime;
                }
                System.out.println("Finished!");
                fileOut.write(dataSize + "\t" + (totalTime / iterCount) + "\n");
                fileOut.flush();
            }
            fileOut.close();
        } catch(Exception e) {
            System.err.println(e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * Times findMin method of PriorityQueue for different data sizes and outputs results to a file.
     */
    private static void timeFindMin() {
        try {
            fileOut = new OutputStreamWriter(new FileOutputStream(new File("assignment11-findMinTimer.csv")));
            PriorityQueue<Integer> findTimer;
            for (int currentPow = startPow; currentPow <= stopPow; currentPow++) {
                findTimer = new PriorityQueue<Integer>();
                int dataSize = (int) Math.pow(2,  currentPow);
                System.out.print("Timing findMin on datasize " + dataSize + " (Power: " + currentPow + ")...");
                long totalTime = 0;
                long stopTime;

                rand = new Random(13);

                for (int currentRand = 0; currentRand < dataSize; currentRand++) {
                    findTimer.add(rand.nextInt());
                }

                long startTime = System.nanoTime();
                while (System.nanoTime() - startTime < 1000000){}

                for (int currentIter = 0; currentIter < iterCount; currentIter++) {
                    startTime = System.nanoTime();
                    findTimer.findMin();
                    stopTime = System.nanoTime();
                    totalTime += stopTime - startTime;
                }
                System.out.println("Finished!");
                fileOut.write(dataSize + "\t" + (totalTime / iterCount) + "\n");
                fileOut.flush();
            }
            fileOut.close();
        } catch(Exception e) {
            System.err.println(e.getMessage());
            e.printStackTrace();
        }
    }
}

This is the graph results I currently have: Timing Results

Thanks in advance!

Here's a hand-wavy argument to the effect that insertion is O(1) average (and O(log n) worst case, of course).

Construct a random binary heap on a random set of elements by assigning the minimum as the root, dividing the remaining elements into random subsets, and constructing a sub-tree on each subset to be a child of the root. (The actual distribution of heaps constructed by random insertion may be different from this, but I'm guessing not materially so.)

Into this heap we insert a random element x . Extending the array is O(1) amortized, so the main cost is up-heap, which is proportional to the number of elements displaced. Let's compute an expected value.

Given that the heap has n existing elements, the probability that the new element is less than the root is 1/(n+1) , resulting in at most log (n+1) displaced elements. Otherwise, the displacement is confined to one of the sub-trees, of (n-1)/2 elements. Both x and the elements of that sub-tree are conditioned on being greater than the root, so we can reason inductively to find that the expected cost is

       log (n+1)
T(n) = --------- + T((n - 1)/2)
         n + 1

T(0) = 0,

whereupon we find that, for n = 2^k - 1 ,

T(2^k - 1) = k/2^k + T(2^(k-1) - 1)
           = sum_{j=0}^k j/2^j
           = O(1) by standard analysis techniques.

(All logarithms are base 2 .)

Have you tried running the add method with a non-random input (for example, on a increasing/decreasing array)? It can make it actually do ~log n actions per insertion. It may not be the case for random input as a newly added element is unlikely to be the largest one and go all the way to the root when you add random elements (it might be the case that the expected value of the number of swaps per insertion is a constant in this case. I haven't tried to actually compute it, though).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM