views:

779

answers:

6

I'm looking for Java solution but any general answer is also OK.

Vector/ArrayList is O(1) for append and retrieve, but O(n) for prepend.

LinkedList (in Java implemented as doubly-linked-list) is O(1) for append and prepend, but O(n) for retrieval.

Deque (ArrayDeque) is O(1) for everything above but cannot retrieve element at arbitrary index.

In my mind a data structure that satisfy the requirement above has 2 growable list inside (one for prepend and one for append) and also stores an offset to determine where to get the element during retrieval.

+2  A: 

Your idea might work. If those are the only operations you need to support, then two Vectors are all you need (call them Head and Tail). To prepend, you append to head, and to append, you append to tail. To access an element, if the index is less than head.Length, then return head[head.Length-1-index], otherwise return tail[index-head.Length]. All of these operations are clearly O(1).

Yuliy
It works fine if we don't need to remove the elements.
Igor Krivokon
It still works if you remove from the head or tail, just not so well if you remove from the middle somewhere. The author didn't state that was a requirement, so I'd say this suggestion is pretty decent. However, people like to claim O(1) but forget that their two arrays may have to resize just like a single one, and if you treat a single array as a circular buffer, you may resize less often. Even so, no one approach seems clearly better than another.
Quinn Taylor
+8  A: 

You're looking for a double-ended queue. This is implemented the way you want in the C++ STL, which is you can index into it, but not in Java, as you noted. You could conceivably roll your own from standard components by using two arrays and storing where "zero" is. This could be wasteful of memory if you end up moving a long way from zero, but if you get too far you can rebase and allow the deque to crawl into a new array.

A more elegant solution that doesn't really require so much fanciness in managing two arrays is to impose a circular array onto a pre-allocated array. This would require implementing push_front, push_back, and the resizing of the array behind it, but the conditions for resizing and such would be much cleaner.

Matthew Curry
It should be noted that adding to a deque is only amortized O(1) time... any individual add operation can be O(n) if a resize is necessary.
Mark Santesson
In the name of clarity for novice readers, "double-ended queue" == "deque". Good answer — I included some of the details for a circular buffer implementation in my answer, too.
Quinn Taylor
A: 

A simple solution is to just use a HashMap mapping Integer indexes to the values. You would store the index of the head and tail as integers

  • An append would then simply be hashMap.put(tailIndex++, value)
  • a prepend could be hashMap.put(headIndex--, value)
  • a get would be hashMap.get(index)

That is amoritzed O(1) for all operations.

The two-array solution you suggested would likely be more efficient however

Chi
You need to adjust the index in the 'get' - it shold be hashMap.get( headIndex + index ).
Hexagon
+3  A: 

If you treat append to a Vector/ArrayList as O(1) - which it really isn't, but might be close enough in practice -
(EDIT - to clarify - append may be amortized constant time, that is - on average, the addition would be O(1), but might be quite a bit worse on spikes. Depending on context and the exact constants involved, this behavior can be deadly).

(This isn't Java, but some made-up language...).

One vector that will be called "Forward". A second vector that will be called "Backwards".

When asked to append - Forward.Append().

When asked to prepend - Backwards.Append().

When asked to query -

if ( Index < Backwards.Size() )
{
    return Backwards[ Backwards.Size() - Index - 1 ]
}
else
{
    return Forward[ Index - Backwards.Size() ]
}

(and also check for the index being out of bounds).

Hexagon
It really *is* O(1) -- amortized. See the proof here: http://www.cs.toronto.edu/~bureshop/my_lec8.ps
tgamblin
Come on! The definition of O(1) is *worst case*. There are other notations for amortized constant time.I can clarify this in my answer - but Vector append is really NOT O(1). If it would be, we wouldn't be needing linked lists...
Hexagon
Added a clarification in the answer for O(1) vs. amortized constant time.
Hexagon
I agree with @tgamblin, appending to a vector is definitely O(1) amortized. Further, the primary strength of linked lists is NOT constant-time append/prepend, it is inserting and deleting nodes at a random location inside the structure, something which arrays are especially poor at. I can't see how adding elements to linked list is any faster in aggregate — you have a ton of small allocations instead of progressively fewer large reallocations (assuming doubling on resize). The result is usually fragmented memory and lots of page faults, whereas arrays have much better locality of reference.
Quinn Taylor
A: 

What you want is a double-ended queue (deque) like the STL has, since Java's ArrayDeque lacks get() for some reason. There were some good suggestions and links to implementations here:

tgamblin
+2  A: 

A deque may be implemented to provide all these operations in O(1) time, although not all implementations do. I've never used Java's ArrayDeque, so I thought you were joking about it not supporting random access, but you're absolutely right — as a "pure" deque, it only allows for easy access at the ends. I can see why, but that sure is annoying...

An ideal way to implement an exceedingly fast deque is to use a circular buffer, especially since you are only interested in adding removing at the front and back. I'm not immediately aware of one in Java, but I've written one in Objective-C as part of an open-source framework. You're welcome to use the code, either as-is or as a pattern for implementing your own.

Here is a WebSVN portal to the code and the related documentation. The real meat is in the CHAbstractCircularBufferCollection.m file — look for the appendObject: and prependObject: methods. There is even a custom enumerator ("iterator" in Java) defined as well. The essential circular buffer logic is fairly trivial, and is captured in these 3 centralized #define macros:

#define transformIndex(index) ((headIndex + index) % arrayCapacity)
#define incrementIndex(index) (index = (index + 1) % arrayCapacity)
#define decrementIndex(index) (index = ((index) ? index : arrayCapacity) - 1)

As you can see in the objectAtIndex: method, all you do to access the Nth element in a deque is array[transformIndex(N)]. Note that I make tailIndex always point to one slot beyond the last stored element, so if headIndex == tailIndex, the array is full, or empty if the size is 0.

Hope that helps. My apologies for posting non-Java code, but the question author did say general answers were acceptable.

Quinn Taylor