
Removing Array Duplicates - ingve
https://flak.tedunangst.com/post/removing-array-duplicates
======
kccqzy
Usually a fine approach would be to construct a hash table as we walk the
array, and if the element is not seen put it in the hash table, otherwise
"remove" it by setting it to the empty string. If the hash table
implementation is good, we can pretty much find out whether an entry has been
seen before in O(1) time, making the whole process O(n).

That's what the author called "alt" and I don't understand why the author
objects to it. The memory needed is usually proportional to the number of
distinct elements in the original array. You don't generally have to copy the
original values (usually you just copy a pointer).

~~~
thedufer
Although the examples are all about strings, there's no reason to be so
specific. One nice property of the algorithms described in the article is that
they only require the elements to be comparable (even better, the first two
implementations only require them to have an equivalence relation), while
yours requires that they be hashable.

~~~
jdmichal
Is there an example of a type that has an equivalence operation but cannot
have a hash operation? Seems to me that those two are identical. If you have a
subset of elements within a type that defines identity, then you can hash that
identity.

~~~
thedufer
Yes, in a garbage-collected language a ref cell is typically unhashable since
the only identifying data about it is the pointer location, which could change
between calls to the hash function. However, you can still define equality
pretty easily (just compare the pointer values). This is also my best example
of something with an equivalence relation that is not comparable.

I don't have a ready example of something that is comparable but not hashable,
but it isn't uncommon in my experience to come across an opaque type whose
author simply didn't expose enough information to hash it, while they did
expose a compare function.

------
saagarjha
The author uses Go in this example, and they're really just struggling against
the language. This is a pretty good example of why choice of language matters:
in C++ this could be a simple, linear pass through the array with an
std::unordered_set storing visited elements, with an optional
std::remove_if/std::erase at the end if they wanted to actually get rid of the
elements. All done in linear time, space overhead, and likely with shorter and
faster code.

~~~
Nimelrian
Yup, you can implement the whole ordeal in C++ like this:

    
    
      int main(int argc, char** argv) {
        std::vector<std::string> strings{
          "apple", "cAt", "cat", "Dog", "apple", "dog", "Cat",
          "Apple", "dOg", "banana", "cat", "dog", "apple",
        };
      
        strings.erase(
          std::remove_if(
            std::begin(strings),
            std::end(strings),
            [seen = std::unordered_set<std::string>{}](std::string_view str) mutable {
              std::string lower = "";
              std::transform(std::begin(str), std::end(str), std::back_inserter(lower), ::tolower);
              if (seen.find(lower) == std::end(seen)) {
                seen.insert(lower);
                return false;
              }
              return true;
            }
          ),
          std::end(strings)
        );
      
        for (auto& str : strings) {
          std::cout << str << "\n";
        };
      }

~~~
nicoburns
Yeah, this is definitely the downside to Go. You can write more or less the
same code as your C++ in Rust like this:

    
    
        use std::collections::HashSet;
        
        fn main () {
            let strings = vec![
              "apple", "cAt", "cat", "Dog", "apple", "dog", "Cat",
              "Apple", "dOg", "banana", "cat", "dog", "apple",
            ];
            
            let mut set : HashSet<&'static str> = HashSet::new();
            
            let strings : Vec<&str> = strings
                .into_iter()
                .filter(|string| set.insert(string))
                .collect();
            
            println!("{:?}", strings);
        }
    

(runnable snippet: [https://play.rust-
lang.org/?version=stable&mode=debug&editio...](https://play.rust-
lang.org/?version=stable&mode=debug&edition=2018&gist=7d19d63ac220173cc7a40d2695841b69))

EDIT: updated to make use of the fact that `set.insert` returns a boolean.

~~~
DougBTX
The output form this is a little different form the article, as it
additionally requires:

* that the uniqueness check is done case-insensitively,

* that the casing from the first instance of the string in the input array should be used in the output, and

* that the output should be ordered based on the order of the first instance of the string in the input array

Possibly an unusual set of requirements (I'd generally expect that if removing
duplicates from a list, that the output order wouldn't matter) but hey,
different problems have different requirements.

The desired output is:

    
    
        apple
        cAt
        Dog
        banana
    

while that code snippet produces:

    
    
        apple
        cAt
        cat
        Dog
        dog
        Cat
        Apple
        dOg
        banana

~~~
lasagnaphil
I've tried to fix the Rust code, but I'm getting some lifetime errors. Can
anyone spot the problem? (Note: I've used C++ for most of the time and am
still uncomfortable with Rust)

    
    
      use std::collections::HashSet;
    
      fn main () {
        let strings = vec![
          "apple", "cAt", "cat", "Dog", "apple", "dog", "Cat",
          "Apple", "dOg", "banana", "cat", "dog", "apple",
        ];
    
        let mut set : HashSet<&'static str> = HashSet::new();
        
        let strings : Vec<&str> = strings
            .into_iter()
            .map(|string| (string, string.to_lowercase()))
            .filter(|(_, lower)| set.insert(lower))
            .map(|(string, _)| string)
            .collect();
        
        println!("{:?}", strings);
      }

~~~
dbaupp
The lower value is of type &String because the argument to filter is of type
&(String, String) and the pattern match needs to propagate the &. The
reference is only valid for the body of the filter closure (that is, it is
&'short String, for some anonymous/unnamed lifetime 'short), but it is being
put into a set that expects &'static str (the explicit type on the let line).
The &String to &str type coercion is fine, but it's not correct to treat the
&'short as a &'static (the short one is potentially invalid after filter's
closure returns).

Changing the type annotations won't make this compile, because it is actually
catching a real bug. The iterator is lazy and the full pipeline is executed
for each element at once: lowercasing, inserting into the set, inserting into
the Vec that's the result of the collect, and deallocating the lower string.
This last step is the key/danger: if the HashSet held references/slices to the
lower strings (instead of owning them), those references would become dangling
immediately and future look-ups into the set won't work right/will trigger
undefined behaviour.

The problem is a little clearer (and mostly fixed) if you simplify the code
slightly by removing the two map calls, and instead call to_lowercase in the
filter directly:

    
    
            .into_iter()
            .filter(|string| set.insert(string.to_lowercase()))
            .collect();
    

This form is a type error, that can be corrected by changing the type
annotation to be HashSet<String>, or even removing it entirely and letting
type inference handle it. The HashSet owning the strings is the key, so they
only disappear after the entire iteration is complete, not after each element.

~~~
DougBTX
Even with the hash set solution, it would be nice to not copy the string
contents, which unfortunately is needed if the hash set contains lowercased
versions of the strings. On the other hand, converting all strings to
lowercase then comparing them isn't a great way to case-insensitively compare
strings anyway, as Unicode doesn't guarantee that will work.

The UniCase crate defines a wrapper around strings with a case-insensitive Eq
implementation, so this works:

    
    
        use std::collections::HashSet;
        use unicase::UniCase;
    
        fn main () {
            let strings = vec![
                "apple", "cAt", "cat", "Dog", "apple", "dog", "Cat",
                "Apple", "dOg", "banana", "cat", "dog", "apple",
            ];
            
            let mut set = HashSet::new();
            
            let strings: Vec<_> = strings
                .iter()
                .filter(|&string| set.insert(UniCase::new(string)))
                .collect();
            
            println!("{:?}", strings);
        }
    

[https://play.rust-
lang.org/?version=stable&mode=debug&editio...](https://play.rust-
lang.org/?version=stable&mode=debug&edition=2018&gist=b8109c4f270266724b14d426ade5bcc8)

~~~
nicoburns
This is pretty cool. People might think it's cheating to pull in a crate. But
IMO the fact that its super easy to pull in a crate in Rust is one of the best
things about the language.

------
KirinDave
This article is incomplete. The Haskell Library "discrimination" has a linear
time, "productive" (in that it can output partial results before the full
input has been seen) version of this:

[http://hackage.haskell.org/package/discrimination-0.3/docs/D...](http://hackage.haskell.org/package/discrimination-0.3/docs/Data-
Discrimination.html#v:nub)

A simpler non-productive algorithm uses sorting, but you can do so in linear
time and we have known about this for some time. Sadly, this information is
not well distributed among software engineers in industry.

~~~
bpicolo
You can get partial results with really any enumerable style api pretty
easily, e.g. python

    
    
        def distinct(l):
            seen = set()
            for s in l:
                if s.lower() not in seen:
                    yield s
                    seen.add(s.lower())

~~~
franey
There's also the classic one-liner:

    
    
        unique_array = list(set(non_unique_array))

~~~
KirinDave
Which is fine. I'm fairly sure that's n log n, although sometimes those end up
being n^2 if they're biased towards memory friendliness.

~~~
nwallin
It's O(n). Python's set has O(1) insertion and lookup.

[https://wiki.python.org/moin/TimeComplexity#set](https://wiki.python.org/moin/TimeComplexity#set)

~~~
KirinDave
The page you link says it's O(n) for lookup.

~~~
nwallin
It says O(1) for lookup. Average case is the one that matters here.

Worst case is for people who've intentionally written bad hash functions.
([https://xkcd.com/221/](https://xkcd.com/221/)) Python has put significant
effort into good default hashes[1] so you won't be vulnerable to attackers
feeding in maliciously colliding hashes, nor will you have performance
degradation from biased bits in the hashes if you've written an insufficiently
uniform hash function.[2]

[1] [https://python-
security.readthedocs.io/vuln/cve-2012-1150_ha...](https://python-
security.readthedocs.io/vuln/cve-2012-1150_hash_dos.html)

[2] For instance, pointers are usually aligned to cache line boundaries, and
are biased toward page boundaries, and in C++, the hash function of an
integer/pointer is the identity function. So in C++, a std::unordered_set of
pointers will have ridiculously high collision rates. If you have 1024
pointers in a set of size 2048, you will only have only 32 buckets that have
values in them, and each of those buckets will have roughly 32 elements, and
the zero bucket will even more. Python doesn't let you shoot yourself in the
foot, and whitens your hashes before going to the dictionary. C++ "solves"
this problem by providing an API which allows you to override the hash
function.

~~~
KirinDave
> It says O(1) for lookup. Average case is the one that matters here.

No, it's actually not. Everyone else and the article were using the worst case
asymptotic, so you'reswitching to this vernacular and suggesting, "Oh we mean
the asymptotic of the average."

Redefining the entire conversation to your notational convenience is not a
very fair call, and doesn't speak well of your intentions.

And what's more, it's typical to use big-theta notation to discuss average
performance anyways. So even if we WERE using that, we'd be using different
notation to be more consistent with the literature and less confusing.

> Worst case is for people who've intentionally written bad hash functions.

No. It's for people who do not know what kind of input they're going to
receive or what kind of hardware they're going to execute on.

------
alkonaut
The code seems to "remove" items from the array by setting an empty string to
them. But in all mainstream languages I know of, an empty string is also a
valid string. So the item isn't "removed" it's simply replaced by the empty
string. Am I completely misunderstanding what the author is trying to do?

Edit: Thanks: I missed the passage about this being the first step of two,
with the compaction coming later (that step isn't in the article at all).

~~~
desdiv
His goal is to get ["apple", "dog", "cat"] from ["apple", "dog", "apple",
"cat", "apple"].

He does it in two steps[0]:

Step ONE: Replace any duplicate values with the empty string (the tombstone)
in the original array. (Going from ["apple", "dog", "apple", "cat", "apple"]
to ["apple", "dog", "", "cat"]).

Step TWO: Compact the original array by creating a new array out of all non-
tombstone values.

He shows 4 different algorithms for performing Step ONE. He shows 0 algorithms
for performing Step TWO because it's trivial.

[0]: >We’d like solutions that conserve time and space. All solutions here
work in place, using the tombstone technique to create a sparse array, then
compacting it later.

~~~
Someone
That means it removes empty strings from the input.

If this were a library function, I don’t think that’s acceptable (rationale:
if you want to remove all empty strings, but the library function doesn’t, you
can easily correct that. If you want to keep the first empty string, but the
library function removes it, calling the library function is as good as
useless)

~~~
jerf
On the flip side, if we're going to be discussing libraries, remember that
things like "Maybe<String>" aren't free. That "Maybe" wrapper in the general
case has to expand the size of the underlying data structure to adjoin an
additional element to it, and that's something else a library may want to
think twice about.

I say "in the general case" because in the _specific_ case it is not always
necessary. ISTR Rust implements a specialization on pointers where you can
syntactically wrap an Option around something and it's smart enough to use the
null pointer directly as the missing case under the hood. However, if you've
got something like a plain ol' machine int, you have to expand it somehow to
get an Option or Maybe, because the compiler is not entitled to remove even a
single element out of those for its purposes.

How acceptable it is depends on your ability to declare an in-range (for the
data type) element as the "impossible" element. Being able to use an in-range
element is more efficient, but less general. In this case, simply by
declaration Ted says empty strings are not valid. The next time he does this,
they may be, and the technique would have to be adapted to that. A library
that uses some equivalent to Option or Maybe is a good safe default for a
library, of course. Another safe default is to create a new array and return
that, too. It's not that hard to end up in a place where the safe default
library is not suitable, although nowadays it takes enough data to choke a
horse to get there since our computers are so darned fast.

~~~
DougBTX
> ISTR Rust implements a specialization on pointers

Yes, and NonZeroU8 for Option<NonZeroU8> and friends:

[https://doc.rust-lang.org/std/num/struct.NonZeroU8.html](https://doc.rust-
lang.org/std/num/struct.NonZeroU8.html)

------
rurban
At least he mentions the name of his technique: setting tombstones.

Of course it's highly problematic not to explain why he choose "" as
tombstone. It looks like a legal array value to me, so offline compaction will
have a hard time to separate a tombstone from an empty string. Without
documentation and assertions that "" is not a legal value on insert, certainly
a bug.

But he fails to list other common useful techniques, without tombstones:

1) if it's the first index to be removed, just advance the array pointer by
one, and keep the original pointer separate for the final free call. very
useful for strings, cutting off prefixes.

2) sparse arrays: he mentioned it, but he doesn't use it in his code. Just
leave out the hole. very useful for matrixes.

3) temporary hashes. if the array is long (like >256), use a temp. hash to
find duplicates.

4) copy to new array, leaving out the duplicates. mostly together with a temp.
hash. but for short arrays a linear search is also doable. he showed the
linear search, but only with tombstones. This is the functional approach,
always copy instead of destructive modifications. This is done in offline
compaction.

5) for large arrays use a bloom filter to find dups. can be tuned to the best
percentage. (This is what I used in my file deduper, which turned out better
than normal hashes)

~~~
viraptor
> not to explain why he choose "" as tombstone

Because his "neutral pseudocode" (go) doesn't allow setting the string entries
in an array to nil. You have to have some value, so commonly you'd use "".

~~~
LordHeini
But one can not add an empty string to the array which is an obvious bug.

There are slices in go which should be used in this case.

~~~
viraptor
It's a toy example in a semi-funny (or trying) post. Did you really expect
handling of all the edge cases?

~~~
rurban
No because it's Ted Unangst writing about "Removing Array Duplicates", the guy
who added string arguments being comma-splitted at run-time instead of using
safe and fast bitmasks to new OpenBSD API's. So we know already that he has no
idea what he's doing, and shouldn't be let to any important API in serious
projects.

But he writes like he has an idea what he's doing, so I had to explain the
common cases for this problem. Using tombstones is a special case and you need
to be lucky to have such a special illegal value available. Most cases don't
allow it, esp. in strongly typed languages.

------
moab
Semisorting, and then running a filter to remove duplicates (which will be
consecutive after the semisort) can solve this problem in O(n) expected work &
space and O(log n) depth w.h.p. AFAIK the approach is reasonably practical as
well. Note that the algorithm is not in-place.

Paper:
[https://people.csail.mit.edu/jshun/semisort.pdf](https://people.csail.mit.edu/jshun/semisort.pdf)

------
hwj
Because the 'neutral pseudocode' was actually valid Go code I just benchmarked
it:

    
    
            BenchmarkDedupe1-4       3000000               521 ns/op
            BenchmarkDedupe2-4       5000000               275 ns/op
            BenchmarkDedupe3-4        500000              3337 ns/op
            BenchmarkAlt-4           1000000              3174 ns/op
    

Source: [https://gitlab.com/hwj/rad](https://gitlab.com/hwj/rad)

(The 'alt' implementation is my own and maybe not what the author had in mind
with 'a searchable data structure to record seen entries'.)

~~~
dan-robertson
Benchmark times are going to vary quite a lot with size of array and number of
unique elements so recording times for one specific size of array doesn’t say
much.

------
Someone
Another alternative: only loop looking for duplicates when a Bloom filter
([https://en.wikipedia.org/wiki/Bloom_filter](https://en.wikipedia.org/wiki/Bloom_filter))
tells you you may have seen an item before.

O(n²) in theory, but typically a lot faster in practice, at small fixed (semi-
fixed, if you size the filter based on the size of the input) overhead.

Also requires you to be able to normalize you strings (for strings,
lowercasing or uppercasing may not be enough, depending on culture)

~~~
masklinn
That's "The alternative which shall not be named" and chances are you'd just
use a set as your secondary data structure, there's limited point to using a
bloom filter unless the input sequence is absolutely humongous.

~~~
hvidgaard
It depends entirely on what you need to guarantee. Using a hash tabel you
guarantee with a very high probability that you only remove duplicated
elements. If you need 100% guarantee, then you must do something else.

~~~
wruza
Hash tables don’t work in a way that you assume here. Original keys don’t get
lost, so you can check whether a key is actually in the table or just makes a
collision.

In its simplest case, HT is an array1 of array2s of key-value pairs, where
array1 is indexed via [hash(key) % a1.length] and then array2 (aka bucket) is
searched linearly for a given key by comparing contents.

~~~
hvidgaard
A hash table does not need to have a collision resolution strategy. It's only
definition is key/value pair. That you can have buckets function to avoid
collisions is "extra" so to speak.

C# linq .Distinct() does not do that, it only checks on the key.

~~~
wruza
A hash table is a well-known term that covers multiple algorithms, but I never
seen one that worked the way you describe. False positive lookups is a
property of bloom filter-like struct, not of a hash table. Using false
positives to remove duplicates would be wrong indeed.

Ed: I missed “It's only definition is key/value pair” part. No, that’s called
key-value list or table. _Hash_ table uses _hashes_ of keys to partition
itself into quick lookup segments - that’s the point.

------
gfldex
I gave this a shot in Perl 6 as can be found at
[https://gist.github.com/8a58c3af3520e127fe7852d834e5e0fa](https://gist.github.com/8a58c3af3520e127fe7852d834e5e0fa)

I build a very simple^1 non-balancing binary tree and skip any insertation
that would reproduce a duplication. Since the tree is not balanced, the order
of insertation is preserved. Search time goes up without a good balance but
the tree will have the same amount of nodes than unique elements in the
original array. Each element is 6 machine words plus some gc overhead. If it's
faster or slower then a hash table depends on the ratio between elements in
the array and the number of duplicates.

1) Perl 6 got a general compare operator that works well with a wide range of
types. So I don't have to care about types at all. If I would have to care I
could monkeytype the operator candidates for custom types into the language.

~~~
0rac1e
Default &transform function should be *.fc

The one-liner solution is: @data.unique(as => &fc)

------
hellllllllooo
My first thought was using a map to store what's been seen and run through the
list keeping copying the values that haven't been seen to a new list in order
or just marking them as dupes in place with the empty string.

This seems to be the "alt" case and is dismissed by the author but would like
to hear a fuller explaination of why this is a problem?

~~~
roel_v
Too slow, too much hashing, too many copies.

~~~
bjoli
It will probably be faster than the code in the article.

~~~
roel_v
How?

~~~
saagarjha
It requires a single linear pass instead of a convoluted sort.

~~~
roel_v
... what sort? Are talking about the same thing here?

~~~
saagarjha
The code in the article has a sort in it.

~~~
KirinDave
Why would that be "more copies" than building up a hash table is what I don't
get.

~~~
bjoli
It requires many passes through the list, and even O(LogN) is probably enough
to make it slower than a hash based O(n) approach depending on list size.
Hashing is fast. If space is an issue, just use an appropriately sized bloom
filter.

------
djpilot
From TFA: "These examples are written in neutral pseudocode that should be
adaptable to most any imperative language."

False. The examples are written in Go, a generally easy to read language.
Maybe the author thought it is a funny joke. Low bar :)

~~~
masklinn
> The examples are written in Go, a generally easy to read language.

But not necessarily so, #3 and #4 are just godawful.

edit: and as you point out below, Go's sorting interface is completely alien
and doesn't easily translate to any other language.

~~~
djpilot
Agreed, the sort interface in go is one of the worst parts, and doesn't
translate to any other language I'm familiar with. The article was mildly
entertaining but by the end left much to be desired.

Shoulda just gone with python, rust, c, crystal lang.. anything else for
sorting!

------
leshow
Why don't you just use a set?

~~~
KirinDave
Constructing sets has a running time, too. All you're really doing when you
build a set is sorting.

~~~
recursive
That's not true for any reasonable set implementation. Sorting is
unnecessarily time-intensive for maintaining distinctness. Hashing is
sufficient.

~~~
KirinDave
Which set implementation are you referring to?

And also remember the context here: you're removing duplicate value's from an
array. So you're inserting N items into a set. If the set insertion or
enumeration involves even a O(log(n)) operation, you're at nlogn.

~~~
recursive
A hashset has O(1) membership testing and amortized O(1) element insertion.

~~~
KirinDave
But it requires a perfect hash. You need to guarantee over any potential input
that you don't have collisions.

How do you do that over all inputs? If there is a generalized non-
probabilistic perfect hash function I am unaware of it and I'd like to be
aware of it.

~~~
recursive
No, it doesn't require a perfect hash. There are collision mitigations. In
practice there several that can get to O(1). I'm not sure if there is a way to
achieve O(1) in the worst case. But the worst case can be made arbitrarily
unlikely, even against adversarial inputs.

~~~
KirinDave
> I'm not sure if there is a way to achieve O(1) in the worst case.

Then you're not at O(1). You cannot say "O(1) except in the worst case". That
is like saying, "It is blue except when you look at it."

But you don't need these mitigations (most of which are O(log n)) because you
never had the hash table. You should look at American Flag sort, because
morally it's actually doing something that closely approximates what you're
thinking about. That's why I brought it up elsewhere in the thread.

~~~
recursive
It's O(1) in the sense that git works. Git uses "commit hashes". If these
collide, something would break, on par with the universe stopping working, I
presume. But I'm sure, since this never actually happens in practice.

~~~
KirinDave
[https://arstechnica.com/information-
technology/2017/02/water...](https://arstechnica.com/information-
technology/2017/02/watershed-sha1-collision-just-broke-the-webkit-repository-
others-may-follow/)

It was a bad design to rely on hashing algorithms to never collide. But it's
worth noting that even SHA1 is much more robust than most hash table
algorithms, which are meant to run even faster.

------
self_awareness
The problem with deduplicating the data can be changed into another problem:
ensuring we won't add the data if it would be duplicated.

So instead of trying to deduplicate the array from author's example, I'd
change the 'add()' method so that it searches through the whole set and
doesn't add the data if it's already added.

So now it doesn't require deduplication code at all.

~~~
Adamantcheese
That's what I though as well. In which case it's better to just keep it in a
set-like structure initially, or just dump your entire array into a set
afterwards. That's linear time, which is loads better than any of the
solutions presented.

------
hnruss
The typical way to solve this problem would be to keep the data stored in a
normalized relational database. There would be a Customer table and a Visits
table. Since the data is normalized, the Customer table would contain only one
entry per customer. There would be no need to dedupe the data if it is kept in
this format.

If moving the data into a normalized relational database is not an option, the
next best option that I can think of is to use a hash table (as mentioned
elsewhere in the HN comments). However, I can't imagine working on a system
where it would make sense to write a custom solution for each type of data
query (unless I was working on a database system).

------
emmelaich
three and four might be close to the

[https://en.wikipedia.org/wiki/Schwartzian_transform](https://en.wikipedia.org/wiki/Schwartzian_transform)

but I like the Schwartzian better :-)

~~~
kwindla
Learning about the Schwartzian transform was one of life's memorable "ah hah"
moments for me. There's a whole set of higher-order layers to this programming
thing!

As others have said, language choice matters.

------
jhallenworld
An interesting question is how can you parallelize it? Suppose the data is
huge, but you have many available threads, or machines..

Suppose there are N nodes, each of which owns part of the array. So one way is
for each node to broadcast each entry to all of the other nodes. As each node
receives a broadcast, it checks its array for duplicates, and deletes them.

------
_zachs
The only thing this post did for me was entrench my thought that I'd rather
write Rust over Go.

As ugly as Rust can be to look at, I'd rather solve this in 5 lines of Rust
that make sense than read through any of those Go solutions.

------
efitz
Isn’t a loop k from 1 to n, with an inner loop k to n, actually order n! (The
article says O n^2) ?

~~~
ishitatsuyuki
Adding up 1+2+...+n equals to n(n-1)/2, which when converted to an order
becomes O(n^2).

